1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-03 17:23:57 +08:00

Merge branch 'master' into ssv2-localisation

This commit is contained in:
Denis Titovets
2025-07-09 15:22:42 +03:00
committed by GitHub
Unverified
24 changed files with 428 additions and 208 deletions
+1 -1
View File
@@ -50,7 +50,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright>
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2024 ppy Pty Ltd <contact@ppy.sh>.
Copyright (c) 2025 ppy Pty Ltd <contact@ppy.sh>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1 -1
View File
@@ -8,7 +8,7 @@
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
<PackageTags>dotnet-new;templates;osu</PackageTags>
<TargetFramework>netstandard2.1</TargetFramework>
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.704.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.709.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+1 -1
View File
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
<language>en-AU</language>
</metadata>
<files>
@@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests;
using osu.Game.Tests.Visual;
using osuTK;
@@ -152,6 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}
[Test]
[FlakyTest]
public void TestSpinPerMinuteOnRewind()
{
double estimatedSpm = 0;
@@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
overallDifficulty = (80 - greatHitWindow) / 6;
overallDifficulty = (79.5 - greatHitWindow) / 6;
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
if (osuAttributes.SliderCount > 0)
@@ -12,9 +12,9 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@@ -83,7 +83,12 @@ namespace osu.Game.Rulesets.Osu.Mods
{
}
public override Judgement CreateJudgement() => new OsuJudgement();
public override Judgement CreateJudgement() => new StrictTrackingTailJudgement();
}
public class StrictTrackingTailJudgement : SliderTailCircle.TailJudgement
{
public override HitResult MinResult => HitResult.LargeTickMiss;
}
private partial class StrictTrackingDrawableSliderTail : DrawableSliderTail
@@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Visual.Menus
{
public partial class TestSceneToolbarRulesetSelector : OsuTestScene
{
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets, OsuGameBase game)
{
TestRuleset.Resources = new TestResourceStore(game.Resources);
Dependencies.CacheAs<RulesetStore>(new TestRulesetStore(rulesets));
Child = new Container
{
RelativeSizeAxes = Axes.X,
Height = Toolbar.HEIGHT,
Child = new ToolbarRulesetSelector(),
};
}
private class TestRulesetStore : RulesetStore
{
public TestRulesetStore(RulesetStore store)
{
AvailableRulesets = store.AvailableRulesets.Append(new TestRuleset().RulesetInfo);
}
public override IEnumerable<RulesetInfo> AvailableRulesets { get; }
}
private class TestRuleset : Ruleset
{
public static IResourceStore<byte[]> Resources { get; set; } = null!;
public override IEnumerable<Mod> GetModsFor(ModType type) => Enumerable.Empty<Mod>();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => null!;
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
public override IResourceStore<byte[]> CreateResourceStore() => Resources;
public override string Description => "Test Ruleset";
public override string ShortName => "test";
}
private class TestResourceStore : ResourceStore<byte[]>
{
public TestResourceStore(IResourceStore<byte[]> store)
: base(store)
{
}
protected override IEnumerable<string> GetFilenames(string name) => base.GetFilenames(name)
.Select(s => s.Replace("UI/ruleset-select-test", "Gameplay/failsound"));
}
}
}
@@ -333,22 +333,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2
#endregion
#region Source grouping
[Test]
public async Task TestGroupingBySource()
{
int total = 0;
var beatmapSets = new List<BeatmapSetInfo>();
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Cool Game", beatmapSets, out var beatmapCoolGame);
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Cool game", beatmapSets, out var beatmapCoolGameB);
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Nice Movie", beatmapSets, out var beatmapNiceMovie);
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = string.Empty, beatmapSets, out var beatmapUnsourced);
var results = await runGrouping(GroupMode.Source, beatmapSets);
assertGroup(results, 0, "Cool Game", new[] { beatmapCoolGame, beatmapCoolGameB }, ref total);
assertGroup(results, 1, "Nice Movie", new[] { beatmapNiceMovie }, ref total);
assertGroup(results, 2, "Unsourced", new[] { beatmapUnsourced }, ref total);
assertTotal(results, total);
}
#endregion
private static async Task<List<CarouselItem>> runGrouping(GroupMode group, List<BeatmapSetInfo> beatmapSets)
{
var groupingFilter = new BeatmapCarouselFilterGrouping(() => new FilterCriteria { Group = group });
var carouselItems = await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None);
// sanity check to ensure no detection of two group items with equal order value.
var groups = carouselItems.Select(i => i.Model).OfType<GroupDefinition>();
foreach (var header in groups)
{
var sameOrder = groups.FirstOrDefault(g => g != header && g.Order == header.Order);
if (sameOrder != null)
Assert.Fail($"Detected two groups with equal order number: \"{header.Title}\" vs. \"{sameOrder.Title}\"");
}
return carouselItems;
return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None);
}
private static void assertGroup(List<CarouselItem> items, int index, string expectedTitle, IEnumerable<BeatmapSetInfo> expectedBeatmapSets, ref int totalItems)
@@ -12,6 +12,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Tournament.IPC;
@@ -42,6 +43,7 @@ namespace osu.Game.Tournament.Screens.Setup
[Resolved]
private TournamentSceneManager? sceneManager { get; set; }
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
private Bindable<Size> windowSize = null!;
[BackgroundDependencyLoader]
@@ -70,7 +72,8 @@ namespace osu.Game.Tournament.Screens.Setup
},
};
api.LocalUser.BindValueChanged(_ => Schedule(reload));
localUser.BindTo(api.LocalUser);
localUser.BindValueChanged(_ => Schedule(reload));
stableInfo.OnStableInfoSaved += () => Schedule(reload);
reload();
}
+16 -16
View File
@@ -10,14 +10,14 @@ namespace osu.Game.Localisation
private const string prefix = @"osu.Game.Resources.Localisation.MenuTip";
/// <summary>
/// "Press Ctrl-T anywhere in the game to toggle the toolbar!"
/// "Press {0} anywhere in the game to toggle the toolbar!"
/// </summary>
public static LocalisableString ToggleToolbarShortcut => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press Ctrl-T anywhere in the game to toggle the toolbar!");
public static LocalisableString ToggleToolbarShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press {0} anywhere in the game to toggle the toolbar!", keybind);
/// <summary>
/// "Press Ctrl-O anywhere in the game to access settings!"
/// "Press {0} anywhere in the game to access settings!"
/// </summary>
public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access settings!");
public static LocalisableString GameSettingsShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press {0} anywhere in the game to access settings!", keybind);
/// <summary>
/// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!"
@@ -40,9 +40,9 @@ namespace osu.Game.Localisation
public static LocalisableString ScreenScalingSettings => new TranslatableString(getKey(@"screen_scaling_settings"), @"Try adjusting the ""Screen Scaling"" mode to change your gameplay or UI area, even in fullscreen!");
/// <summary>
/// "What used to be &quot;osu!direct&quot; is available to all users just like on the website. You can access it anywhere using Ctrl-B!"
/// "What used to be &quot;osu!direct&quot; is available to all users just like on the website. You can access it anywhere using {0}!"
/// </summary>
public static LocalisableString FreeOsuDirect => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using Ctrl-B!");
public static LocalisableString FreeOsuDirect(LocalisableString keybind) => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using {0}!", keybind);
/// <summary>
/// "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!"
@@ -75,9 +75,9 @@ namespace osu.Game.Localisation
public static LocalisableString ToggleAdvancedFPSCounter => new TranslatableString(getKey(@"toggle_advanced_fps_counter"), @"Toggle advanced frame / thread statistics with Ctrl-F11!");
/// <summary>
/// "You can pause during a replay by pressing Space!"
/// "You can pause during a replay by pressing {0}!"
/// </summary>
public static LocalisableString ReplayPausing => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing Space!");
public static LocalisableString ReplayPausing(LocalisableString keybind) => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing {0}!", keybind);
/// <summary>
/// "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"
@@ -85,9 +85,9 @@ namespace osu.Game.Localisation
public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!");
/// <summary>
/// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"
/// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via {0}!"
/// </summary>
public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!");
public static LocalisableString SkinEditor(LocalisableString keybind) => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via {0}!", keybind);
/// <summary>
/// "You can create mod presets to make toggling your favourite mod combinations easier!"
@@ -100,14 +100,14 @@ namespace osu.Game.Localisation
public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!");
/// <summary>
/// "Press Ctrl-Shift-R to switch to a random skin!"
/// "Press {0} to switch to a random skin!"
/// </summary>
public static LocalisableString RandomSkinShortcut => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press Ctrl-Shift-R to switch to a random skin!");
public static LocalisableString RandomSkinShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press {0} to switch to a random skin!", keybind);
/// <summary>
/// "While watching a replay, press Ctrl-H to toggle replay settings!"
/// "While watching a replay, press {0} to toggle replay settings!"
/// </summary>
public static LocalisableString ToggleReplaySettingsShortcut => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press Ctrl-H to toggle replay settings!");
public static LocalisableString ToggleReplaySettingsShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press {0} to toggle replay settings!", keybind);
/// <summary>
/// "You can easily copy the mods from scores on a leaderboard by right-clicking on them!"
@@ -140,9 +140,9 @@ namespace osu.Game.Localisation
public static LocalisableString GlobalStatisticsShortcut => new TranslatableString(getKey(@"global_statistics_shortcut"), @"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!");
/// <summary>
/// "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!"
/// "When your gameplay HUD is hidden, you can press and hold {0} to view it temporarily!"
/// </summary>
public static LocalisableString PeekHUDWhenHidden => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!");
public static LocalisableString PeekHUDWhenHidden(LocalisableString keybind) => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold {0} to view it temporarily!", keybind);
/// <summary>
/// "Drag and drop any image into the skin editor to load it in quickly!"
+7 -6
View File
@@ -9,6 +9,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Threading;
@@ -64,7 +65,6 @@ namespace osu.Game.Online.Chat
public IBindableList<Channel> AvailableChannels => availableChannels;
private readonly IAPIProvider api;
private readonly IChatClient chatClient;
[Resolved]
private UserLookupCache users { get; set; }
@@ -72,6 +72,7 @@ namespace osu.Game.Online.Chat
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private ScheduledDelegate scheduledAck;
private IChatClient chatClient = null!;
private long? lastSilenceMessageId;
private uint? lastSilenceId;
@@ -79,14 +80,13 @@ namespace osu.Game.Online.Chat
{
this.api = api;
chatClient = api.GetChatClient();
CurrentChannel.ValueChanged += currentChannelChanged;
}
[BackgroundDependencyLoader]
private void load()
{
chatClient = api.GetChatClient();
chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch));
chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs));
@@ -282,8 +282,7 @@ namespace osu.Game.Online.Chat
// Check if the user has joined the requested channel already.
// This uses the channel name for comparison as the PM user's username is unavailable after a restart.
var privateChannel = JoinedChannels.FirstOrDefault(
c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase));
var privateChannel = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase));
if (privateChannel != null)
{
@@ -645,7 +644,9 @@ namespace osu.Game.Online.Chat
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
chatClient?.Dispose();
if (chatClient.IsNotNull())
chatClient.Dispose();
}
}
@@ -5,10 +5,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Users;
@@ -35,6 +37,8 @@ namespace osu.Game.Online
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
private readonly Dictionary<string, UserStatistics> statisticsCache = new Dictionary<string, UserStatistics>();
/// <summary>
@@ -48,7 +52,8 @@ namespace osu.Game.Online
{
base.LoadComplete();
api.LocalUser.BindValueChanged(_ =>
localUser.BindTo(api.LocalUser);
localUser.BindValueChanged(_ =>
{
// queuing up requests directly on user change is unsafe, as the API status may have not been updated yet.
// schedule a frame to allow the API to be in its correct state sending requests.
+1 -1
View File
@@ -151,7 +151,7 @@ namespace osu.Game.Online
base.Dispose(isDisposing);
if (notificationsClient.IsNotNull())
notificationsClient.MessageReceived += notifyAboutForcedDisconnection;
notificationsClient.MessageReceived -= notifyAboutForcedDisconnection;
if (spectatorClient.IsNotNull())
spectatorClient.Disconnecting -= notifyAboutForcedDisconnection;
+115 -114
View File
@@ -113,6 +113,9 @@ namespace osu.Game
/// </summary>
public const float SCREEN_EDGE_MARGIN = 12f;
private const double general_log_debounce = 60000;
private const string tablet_log_prefix = @"[Tablet] ";
public Toolbar Toolbar { get; private set; }
private ChatOverlay chatOverlay;
@@ -241,12 +244,26 @@ namespace osu.Game
/// </summary>
public virtual bool HideUnlicensedContent => false;
private bool tabletLogNotifyOnWarning = true;
private bool tabletLogNotifyOnError = true;
private int generalLogRecentCount;
public OsuGame(string[] args = null)
{
this.args = args;
forwardGeneralLogsToNotifications();
forwardTabletLogsToNotifications();
Logger.NewEntry += forwardGeneralLogToNotifications;
Logger.NewEntry += forwardTabletLogToNotifications;
Schedule(() =>
{
ITabletHandler tablet = Host.AvailableInputHandlers.OfType<ITabletHandler>().SingleOrDefault();
tablet?.Tablet.BindValueChanged(_ =>
{
tabletLogNotifyOnWarning = true;
tabletLogNotifyOnError = true;
}, true);
});
}
#region IOverlayManager
@@ -347,40 +364,42 @@ namespace osu.Game
if (host.Window != null)
{
host.Window.CursorState |= CursorState.Hidden;
host.Window.DragDrop += path =>
{
// on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
{
HandleLink(path);
return;
}
lock (dragDropFiles)
{
dragDropFiles.Add(path);
Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import");
// File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
// In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
dragDropImportSchedule?.Cancel();
dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100);
}
};
host.Window.DragDrop += onWindowDragDrop;
}
}
private void handlePendingDragDropImports()
private void onWindowDragDrop(string path)
{
// on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
{
HandleLink(path);
return;
}
lock (dragDropFiles)
{
Logger.Log($"Handling batch import of {dragDropFiles.Count} files");
dragDropFiles.Add(path);
string[] paths = dragDropFiles.ToArray();
dragDropFiles.Clear();
Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import");
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
// File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
// In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
dragDropImportSchedule?.Cancel();
dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100);
}
void handlePendingDragDropImports()
{
lock (dragDropFiles)
{
Logger.Log($"Handling batch import of {dragDropFiles.Count} files");
string[] paths = dragDropFiles.ToArray();
dragDropFiles.Clear();
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
}
}
}
@@ -1009,7 +1028,14 @@ namespace osu.Game
detachedBeatmapStore?.Dispose();
base.Dispose(isDisposing);
SentryLogger.Dispose();
if (Host?.Window != null)
Host.Window.DragDrop -= onWindowDragDrop;
Logger.NewEntry -= forwardGeneralLogToNotifications;
Logger.NewEntry -= forwardTabletLogToNotifications;
}
protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults()
@@ -1365,115 +1391,90 @@ namespace osu.Game
overlay.Depth = (float)-Clock.CurrentTime;
}
private void forwardGeneralLogsToNotifications()
private void forwardGeneralLogToNotifications(LogEntry entry)
{
int recentLogCount = 0;
if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return;
const double debounce = 60000;
if (entry.Exception is SentryOnlyDiagnosticsException)
return;
Logger.NewEntry += entry =>
const int short_term_display_limit = 3;
if (generalLogRecentCount < short_term_display_limit)
{
if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return;
if (entry.Exception is SentryOnlyDiagnosticsException)
return;
const int short_term_display_limit = 3;
if (recentLogCount < short_term_display_limit)
Schedule(() => Notifications.Post(new SimpleErrorNotification
{
Schedule(() => Notifications.Post(new SimpleErrorNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
}));
}
else if (recentLogCount == short_term_display_limit)
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
}));
}
else if (generalLogRecentCount == short_term_display_limit)
{
string logFile = Logger.GetLogger(entry.Target.Value).Filename;
Schedule(() => Notifications.Post(new SimpleNotification
{
string logFile = Logger.GetLogger(entry.Target.Value).Filename;
Schedule(() => Notifications.Post(new SimpleNotification
Icon = FontAwesome.Solid.EllipsisH,
Text = NotificationsStrings.SubsequentMessagesLogged,
Activated = () =>
{
Icon = FontAwesome.Solid.EllipsisH,
Text = NotificationsStrings.SubsequentMessagesLogged,
Activated = () =>
{
Logger.Storage.PresentFileExternally(logFile);
return true;
}
}));
}
Logger.Storage.PresentFileExternally(logFile);
return true;
}
}));
}
Interlocked.Increment(ref recentLogCount);
Scheduler.AddDelayed(() => Interlocked.Decrement(ref recentLogCount), debounce);
};
Interlocked.Increment(ref generalLogRecentCount);
Scheduler.AddDelayed(() => Interlocked.Decrement(ref generalLogRecentCount), general_log_debounce);
}
private void forwardTabletLogsToNotifications()
private void forwardTabletLogToNotifications(LogEntry entry)
{
const string tablet_prefix = @"[Tablet] ";
if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_log_prefix, StringComparison.OrdinalIgnoreCase))
return;
bool notifyOnWarning = true;
bool notifyOnError = true;
string message = entry.Message.Replace(tablet_log_prefix, string.Empty);
Logger.NewEntry += entry =>
if (entry.Level == LogLevel.Error)
{
if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_prefix, StringComparison.OrdinalIgnoreCase))
if (!tabletLogNotifyOnError)
return;
string message = entry.Message.Replace(tablet_prefix, string.Empty);
tabletLogNotifyOnError = false;
if (entry.Level == LogLevel.Error)
Schedule(() =>
{
if (!notifyOnError)
return;
notifyOnError = false;
Schedule(() =>
Notifications.Post(new SimpleNotification
{
Notifications.Post(new SimpleNotification
{
Text = NotificationsStrings.TabletSupportDisabledDueToError(message),
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.RedDark,
});
// We only have one tablet handler currently.
// The loop here is weakly guarding against a future where more than one is added.
// If this is ever the case, this logic needs adjustment as it should probably only
// disable the relevant tablet handler rather than all.
foreach (var tabletHandler in Host.AvailableInputHandlers.OfType<ITabletHandler>())
tabletHandler.Enabled.Value = false;
});
}
else if (notifyOnWarning)
{
Schedule(() => Notifications.Post(new SimpleNotification
{
Text = NotificationsStrings.EncounteredTabletWarning,
Text = NotificationsStrings.TabletSupportDisabledDueToError(message),
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.YellowDark,
Activated = () =>
{
OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn);
return true;
}
}));
IconColour = Colours.RedDark,
});
notifyOnWarning = false;
}
};
Schedule(() =>
// We only have one tablet handler currently.
// The loop here is weakly guarding against a future where more than one is added.
// If this is ever the case, this logic needs adjustment as it should probably only
// disable the relevant tablet handler rather than all.
foreach (var tabletHandler in Host.AvailableInputHandlers.OfType<ITabletHandler>())
tabletHandler.Enabled.Value = false;
});
}
else if (tabletLogNotifyOnWarning)
{
ITabletHandler tablet = Host.AvailableInputHandlers.OfType<ITabletHandler>().SingleOrDefault();
tablet?.Tablet.BindValueChanged(_ =>
Schedule(() => Notifications.Post(new SimpleNotification
{
notifyOnWarning = true;
notifyOnError = true;
}, true);
});
Text = NotificationsStrings.EncounteredTabletWarning,
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.YellowDark,
Activated = () =>
{
OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn);
return true;
}
}));
tabletLogNotifyOnWarning = false;
}
}
private Task asyncLoadStream;
@@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.IO.Stores;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@@ -32,6 +33,8 @@ namespace osu.Game.Overlays.Toolbar
private readonly Dictionary<RulesetInfo, SampleChannel> rulesetSelectionChannel = new Dictionary<RulesetInfo, SampleChannel>();
private Sample defaultSelectSample;
private ISampleStore samples;
public ToolbarRulesetSelector()
{
RelativeSizeAxes = Axes.Y;
@@ -39,7 +42,7 @@ namespace osu.Game.Overlays.Toolbar
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load(AudioManager audio, OsuGameBase game)
{
AddRangeInternal(new[]
{
@@ -66,8 +69,15 @@ namespace osu.Game.Overlays.Toolbar
},
});
var store = new ResourceStore<byte[]>(game.Resources);
samples = audio.GetSampleStore(new NamespacedResourceStore<byte[]>(store, "Samples"), audio.SampleMixer);
foreach (var r in Rulesets.AvailableRulesets)
rulesetSelectionSample[r] = audio.Samples.Get($@"UI/ruleset-select-{r.ShortName}");
{
store.AddStore(r.CreateInstance().CreateResourceStore());
rulesetSelectionSample[r] = samples.Get($@"UI/ruleset-select-{r.ShortName}");
}
defaultSelectSample = audio.Samples.Get(@"UI/default-select");
@@ -159,5 +169,12 @@ namespace osu.Game.Overlays.Toolbar
return false;
}
protected override void Dispose(bool isDisposing)
{
samples?.Dispose();
base.Dispose(isDisposing);
}
}
}
+100 -33
View File
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -12,6 +13,8 @@ using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
using osu.Game.Localisation;
@@ -27,6 +30,9 @@ namespace osu.Game.Screens.Menu
private Bindable<bool> showMenuTips = null!;
[Resolved]
private RealmKeyBindingStore keyBindingStore { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
@@ -97,42 +103,103 @@ namespace osu.Game.Screens.Menu
.FadeOutFromOne(2000, Easing.OutQuint);
}
private const int available_tips = 29;
private LocalisableString getRandomTip()
{
LocalisableString[] tips =
{
MenuTipStrings.ToggleToolbarShortcut,
MenuTipStrings.GameSettingsShortcut,
MenuTipStrings.DynamicSettings,
MenuTipStrings.NewFeaturesAreComingOnline,
MenuTipStrings.UIScalingSettings,
MenuTipStrings.ScreenScalingSettings,
MenuTipStrings.FreeOsuDirect,
MenuTipStrings.ReplaySeeking,
MenuTipStrings.MultithreadingSupport,
MenuTipStrings.TryNewMods,
MenuTipStrings.EmbeddedWebContent,
MenuTipStrings.BeatmapRightClick,
MenuTipStrings.TemporaryDeleteOperations,
MenuTipStrings.DiscoverPlaylists,
MenuTipStrings.ToggleAdvancedFPSCounter,
MenuTipStrings.GlobalStatisticsShortcut,
MenuTipStrings.ReplayPausing,
MenuTipStrings.ConfigurableHotkeys,
MenuTipStrings.PeekHUDWhenHidden,
MenuTipStrings.SkinEditor,
MenuTipStrings.DragAndDropImageInSkinEditor,
MenuTipStrings.ModPresets,
MenuTipStrings.ModCustomisationSettings,
MenuTipStrings.RandomSkinShortcut,
MenuTipStrings.ToggleReplaySettingsShortcut,
MenuTipStrings.CopyModsFromScore,
MenuTipStrings.AutoplayBeatmapShortcut,
MenuTipStrings.LazerIsNotAWord,
MenuTipStrings.RightMouseAbsoluteScroll,
};
int tipIndex = RNG.Next(0, available_tips);
return tips[RNG.Next(0, tips.Length)];
switch (tipIndex)
{
case 0:
return MenuTipStrings.ToggleToolbarShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleToolbar).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 1:
return MenuTipStrings.GameSettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSettings).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 2:
return MenuTipStrings.DynamicSettings;
case 3:
return MenuTipStrings.NewFeaturesAreComingOnline;
case 4:
return MenuTipStrings.UIScalingSettings;
case 5:
return MenuTipStrings.ScreenScalingSettings;
case 6:
return MenuTipStrings.FreeOsuDirect(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleBeatmapListing).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 7:
return MenuTipStrings.ReplaySeeking;
case 8:
return MenuTipStrings.MultithreadingSupport;
case 9:
return MenuTipStrings.TryNewMods;
case 10:
return MenuTipStrings.EmbeddedWebContent;
case 11:
return MenuTipStrings.BeatmapRightClick;
case 12:
return MenuTipStrings.TemporaryDeleteOperations;
case 13:
return MenuTipStrings.DiscoverPlaylists;
case 14:
return MenuTipStrings.ToggleAdvancedFPSCounter;
case 15:
return MenuTipStrings.GlobalStatisticsShortcut;
case 16:
return MenuTipStrings.ReplayPausing(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.TogglePauseReplay).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 17:
return MenuTipStrings.ConfigurableHotkeys;
case 18:
return MenuTipStrings.PeekHUDWhenHidden(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.HoldForHUD).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 19:
return MenuTipStrings.SkinEditor(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSkinEditor).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 20:
return MenuTipStrings.DragAndDropImageInSkinEditor;
case 21:
return MenuTipStrings.ModPresets;
case 22:
return MenuTipStrings.ModCustomisationSettings;
case 23:
return MenuTipStrings.RandomSkinShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.RandomSkin).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 24:
return MenuTipStrings.ToggleReplaySettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleReplaySettings).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 25:
return MenuTipStrings.CopyModsFromScore;
case 26:
return MenuTipStrings.AutoplayBeatmapShortcut;
case 27:
return MenuTipStrings.LazerIsNotAWord;
case 28:
return MenuTipStrings.RightMouseAbsoluteScroll;
}
return string.Empty;
}
}
}
+3 -4
View File
@@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
@@ -56,8 +56,7 @@ namespace osu.Game.Screens.Select.Filter
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.RankedStatus))]
RankedStatus,
// added for convenience when changing in this pr: https://github.com/ppy/osu/pull/33889
// [LocalisableDescription(typeof(SortStrings), nameof(SortStrings.Source))]
// Source,
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.Source))]
Source,
}
}
+25 -3
View File
@@ -822,9 +822,31 @@ namespace osu.Game.Screens.SelectV2
/// <summary>
/// Defines a grouping header for a set of carousel items.
/// </summary>
/// <param name="Order">The order of this group in the carousel, sorted using ascending order.</param>
/// <param name="Title">The title of this group.</param>
public record GroupDefinition(int Order, string Title);
public record GroupDefinition
{
/// <summary>
/// The order of this group in the carousel, sorted using ascending order.
/// </summary>
public int Order { get; }
/// <summary>
/// The title of this group.
/// </summary>
public string Title { get; }
private readonly string uncasedTitle;
public GroupDefinition(int order, string title)
{
Order = order;
Title = title;
uncasedTitle = title.ToLowerInvariant();
}
public virtual bool Equals(GroupDefinition? other) => uncasedTitle == other?.uncasedTitle;
public override int GetHashCode() => HashCode.Combine(uncasedTitle);
}
/// <summary>
/// Defines a grouping header for a set of carousel items grouped by star difficulty.
@@ -202,6 +202,9 @@ namespace osu.Game.Screens.SelectV2
return defineGroupByLength(length);
}, items);
case GroupMode.Source:
return getGroupsBy(b => defineGroupBySource(b.BeatmapSet!.Metadata.Source), items);
// TODO: need implementation
//
// case GroupMode.Collections:
@@ -225,6 +228,7 @@ namespace osu.Game.Screens.SelectV2
{
return items.GroupBy(i => getGroup((BeatmapInfo)i.Model))
.OrderBy(s => s.Key.Order)
.ThenBy(s => s.Key.Title)
.Select(g => new GroupMapping(g.Key, g.ToList()))
.ToList();
}
@@ -355,6 +359,14 @@ namespace osu.Game.Screens.SelectV2
return new GroupDefinition(11, "Over 10 minutes");
}
private GroupDefinition defineGroupBySource(string source)
{
if (string.IsNullOrEmpty(source))
return new GroupDefinition(1, "Unsourced");
return new GroupDefinition(0, source);
}
private static T? aggregateMax<T>(BeatmapInfo b, Func<BeatmapInfo, T> func)
{
var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden);
+4 -3
View File
@@ -82,10 +82,11 @@ namespace osu.Game.Tests.Visual
[TearDownSteps]
public virtual void TearDownSteps()
{
if (DebugUtils.IsNUnitRunning && Game != null)
if (DebugUtils.IsNUnitRunning)
{
AddStep("exit game", () => Game.Exit());
AddUntilStep("wait for game exit", () => Game.Parent == null);
AddStep("exit game", () => Game?.Exit());
AddUntilStep("wait for game exit", () => Game?.Parent == null);
AddStep("dispose game", () => Game?.Dispose());
}
}
+2 -2
View File
@@ -35,8 +35,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.704.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.707.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.709.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.708.0" />
<PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.39.0" />
+1 -1
View File
@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.704.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.709.1" />
</ItemGroup>
</Project>