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:
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 "osu!direct" is available to all users just like on the website. You can access it anywhere using Ctrl-B!"
|
||||
/// "What used to be "osu!direct" 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!"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user