From 6eee9dba807d9ea3b8a25af225a1bbfc6e1424d3 Mon Sep 17 00:00:00 2001 From: LA <1245661240@qq.com> Date: Sun, 25 May 2025 03:02:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E6=9B=B4=E6=96=B0=EF=BC=8C=E5=8F=AF?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89note=E5=8F=8A=E9=85=8D=E5=A5=97?= =?UTF-8?q?=E7=89=B9=E6=95=88=EF=BC=8C=E5=90=8C=E6=AD=A5=E5=AE=98=E6=96=B9?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=AD=89=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- osu.Desktop/OsuGameDesktop.cs | 11 + osu.Desktop/Program.cs | 14 +- osu.Desktop/Updater/VelopackUpdateManager.cs | 39 ++- .../Skinning/TestSceneHoldNote.cs | 2 +- .../TestSceneReplayRecording.cs | 6 +- .../TestSceneReplayRewinding.cs | 125 +++++++++ .../ManiaRulesetConfigManager.cs | 17 +- .../Judgements/HoldNoteJudgementResult.cs | 48 ++++ .../LAsEZMania/EzStageDefinitionExtensions.cs | 125 +++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 51 +--- .../ManiaSettingsSubsection.cs | 22 +- .../Mods/ManiaModNoRelease.cs | 2 +- .../YuLiangSSSMods/ManiaModNoLNJudgement.cs | 4 +- .../YuLiangSSSMods/ManiaModReleaseAdjust.cs | 2 +- .../Mods/YuLiangSSSMods/ManiaModRemedy.cs | 10 +- .../Objects/Drawables/DrawableHoldNote.cs | 50 ++-- .../Objects/Drawables/DrawableHoldNoteBody.cs | 2 +- .../Objects/Ez2AcHoldNote.cs | 6 +- .../Objects/MalodyHoldNote.cs | 4 +- .../Objects/NoJudgmentHoldNote.cs | 2 +- ...onvertor.cs => EzManiaHitModeConvertor.cs} | 4 +- ...tTimingInfo.cs => EzManiaHitTimingInfo.cs} | 4 +- .../Scoring/ManiaScoreProcessor.cs | 4 +- .../Skinning/Argon/ArgonHoldBodyPiece.cs | 2 +- .../Skinning/Argon/ArgonHoldNoteTailPiece.cs | 2 +- .../Skinning/BindableExtensions.cs | 15 -- .../Skinning/Default/DefaultBodyPiece.cs | 2 +- .../Skinning/Ez2/Ez2HUD/EzComComboCounter.cs | 4 +- .../Skinning/Ez2/Ez2HUD/EzComComboSprite.cs | 4 +- .../Skinning/Ez2/Ez2HUD/EzComHitTiming.cs | 8 +- .../Ez2/Ez2HUD/EzComHitTimingColumns.cs | 8 +- .../Skinning/Ez2/Ez2HitExplosion.cs | 2 +- .../Skinning/Ez2/Ez2HoldBodyPiece.cs | 2 +- .../Skinning/Ez2/ManiaEz2SkinTransformer.cs | 184 +++---------- .../Skinning/Legacy/LegacyBodyPiece.cs | 2 +- .../Skinning/SbI/ManiaSbISkinTransformer.cs | 2 +- .../UI/DrawableManiaRuleset.cs | 8 +- .../UI/ManiaScrollingStyle.cs | 21 -- .../Mods/TestSceneOsuModStrictTracking.cs | 54 ++++ .../TestSceneOsuAnalysisContainer.cs | 34 ++- .../TestSceneReplayRecording.cs | 12 +- .../Edit/OsuHitObjectComposer.cs | 2 +- .../Mods/OsuModStrictTracking.cs | 4 + .../TestSceneReplayRecording.cs | 18 +- .../Rulesets/Scoring/ScoreProcessorTest.cs | 59 ++++ .../Visual/Beatmaps/TestSceneBeatmapCard.cs | 67 ++++- .../TestSceneBeatmapSetOnlineStatusPill.cs | 10 +- ...estSceneSoloGameplayLeaderboardProvider.cs | 3 + .../TestSceneMultiplayerPositionDisplay.cs | 34 +++ .../TestSceneScreenFooterNavigation.cs | 42 +++ .../Ranking/TestSceneSoloResultsScreen.cs | 16 +- .../TestSceneBeatmapMetadataDisplay.cs | 4 +- .../SongSelectV2/BeatmapCarouselTestScene.cs | 4 + .../SongSelectV2/SongSelectTestScene.cs | 18 +- .../SongSelectV2/TestSceneBeatmapCarousel.cs | 22 ++ .../TestSceneBeatmapCarouselFiltering.cs | 21 ++ .../TestSceneBeatmapCarouselNoGrouping.cs | 34 +++ .../TestSceneBeatmapLeaderboardWedge.cs | 10 +- .../TestSceneBeatmapMetadataWedge.cs | 112 ++++++-- .../SongSelectV2/TestSceneSongSelect.cs | 6 +- .../TestSceneSongSelectFiltering.cs | 22 ++ .../TestSceneModEffectPreviewPanel.cs | 2 +- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 23 +- .../Beatmaps/BeatmapSetNominationStatus.cs | 4 +- .../BeatmapSetNominationStatusRequiredMeta.cs | 25 ++ .../Drawables/BeatmapSetOnlineStatusPill.cs | 16 +- .../Cards/Statistics/NominationsStatistic.cs | 23 +- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 32 ++- .../Beatmaps/Drawables/StarRatingDisplay.cs | 6 +- osu.Game/Configuration/ReleaseStream.cs | 8 +- osu.Game/Database/MemoryCachingComponent.cs | 10 +- .../Localisation/GeneralSettingsStrings.cs | 12 + .../Requests/Responses/APIScoresCollection.cs | 3 + .../Online/Leaderboards/LeaderboardManager.cs | 12 +- osu.Game/Online/Spectator/SpectatorClient.cs | 15 +- osu.Game/OsuGame.cs | 9 +- osu.Game/OsuGameBase.cs | 32 ++- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 2 - .../Sections/General/UpdateSettings.cs | 42 ++- .../Overlays/Settings/Sections/SkinSection.cs | 1 + .../Overlays/Toolbar/AnalogClockDisplay.cs | 12 +- osu.Game/Overlays/Toolbar/ClockDisplay.cs | 8 + .../Rulesets/Judgements/JudgementResult.cs | 5 + .../Objects/Drawables/DrawableHitObject.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 11 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 + osu.Game/Rulesets/UI/IHasRecordingHandler.cs | 2 +- osu.Game/Rulesets/UI/ReplayRecorder.cs | 21 +- osu.Game/Rulesets/UI/RulesetInputManager.cs | 1 + .../Backgrounds/BackgroundScreenBeatmap.cs | 2 - .../Backgrounds/BackgroundScreenDefault.cs | 2 +- .../Backgrounds/VideoBackgroundScreen.cs | 10 +- .../Components/SelectionBoxRotationHandle.cs | 4 +- osu.Game/Screens/Edit/Editor.cs | 31 ++- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 32 ++- osu.Game/Screens/EzSkinSettings.cs | 218 +++++++++++++-- osu.Game/Screens/Footer/ScreenFooterButton.cs | 1 + .../Multiplayer/MultiplayerPositionDisplay.cs | 6 +- .../Ranking/LAsAnalysisOptionsPopover.cs | 4 +- ...etDropdown.cs => ManiaRulesetDropdown.txt} | 0 osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 14 +- .../SelectV2/BeatmapCarouselFilterMatching.cs | 15 +- .../SelectV2/BeatmapLeaderboardWedge.cs | 15 +- .../Screens/SelectV2/BeatmapMetadataWedge.cs | 1 + .../BeatmapMetadataWedge_MetadataDisplay.cs | 10 +- osu.Game/Screens/SelectV2/PanelBeatmap.cs | 17 +- osu.Game/Screens/SelectV2/PanelBeatmapSet.cs | 1 + .../SelectV2/PanelBeatmapStandalone.cs | 17 +- osu.Game/Screens/SelectV2/SongSelect.cs | 24 +- .../Components}/EzComHitResultScore.cs | 96 ++++--- .../Skinning/Components/EzComScoreCounter.cs | 4 +- osu.Game/Skinning/Components/EzComboText.cs | 4 +- .../Skinning/Components/EzEnumListSelector.cs | 136 ---------- .../Skinning/Components/EzGetNoteTexture.cs | 92 +++++++ osu.Game/Skinning/Components/EzHitFactory.cs | 105 ++++++++ .../Skinning/Components/EzNoteContainer.cs | 75 ++++++ osu.Game/Skinning/Components/EzScoreText.cs | 4 +- .../Skinning/Components/EzSelectorEnumList.cs | 237 ++++++++++++++++ .../Skinning/Components/EzSelectorTextures.cs | 12 +- osu.Game/Skinning/EzStyleProSkin.cs | 252 ++++++++++++++++++ osu.Game/Skinning/SkinInfo.cs | 1 + osu.Game/Skinning/SkinManager.cs | 1 + .../Drawables/DrawableStoryboard.cs | 5 + osu.sln | 27 +- qodana.yaml | 2 + 125 files changed, 2467 insertions(+), 781 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneReplayRewinding.cs create mode 100644 osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgementResult.cs rename osu.Game.Rulesets.Mania/Scoring/{ManiaHitModeConvertor.cs => EzManiaHitModeConvertor.cs} (98%) rename osu.Game.Rulesets.Mania/Scoring/{ManiaHitTimingInfo.cs => EzManiaHitTimingInfo.cs} (79%) delete mode 100644 osu.Game.Rulesets.Mania/Skinning/BindableExtensions.cs delete mode 100644 osu.Game.Rulesets.Mania/UI/ManiaScrollingStyle.cs create mode 100644 osu.Game/Beatmaps/BeatmapSetNominationStatusRequiredMeta.cs rename osu.Game/Screens/Select/Filter/{ManiaRulesetDropdown.cs => ManiaRulesetDropdown.txt} (100%) rename {osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD => osu.Game/Skinning/Components}/EzComHitResultScore.cs (88%) delete mode 100644 osu.Game/Skinning/Components/EzEnumListSelector.cs create mode 100644 osu.Game/Skinning/Components/EzGetNoteTexture.cs create mode 100644 osu.Game/Skinning/Components/EzHitFactory.cs create mode 100644 osu.Game/Skinning/Components/EzNoteContainer.cs create mode 100644 osu.Game/Skinning/Components/EzSelectorEnumList.cs create mode 100644 osu.Game/Skinning/EzStyleProSkin.cs diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index c33608832f..7290761d56 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -17,6 +17,7 @@ using osu.Framework.Logging; using osu.Game.Updater; using osu.Desktop.Windows; using osu.Framework.Allocation; +using osu.Game.Configuration; using osu.Game.IO; using osu.Game.IPC; using osu.Game.Online.Multiplayer; @@ -33,6 +34,8 @@ namespace osu.Desktop [Cached(typeof(IHighPerformanceSessionManager))] private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager(); + public bool IsFirstRun { get; init; } + public OsuGameDesktop(string[]? args = null) : base(args) { @@ -104,6 +107,14 @@ namespace osu.Desktop protected override UpdateManager CreateUpdateManager() { + // If this is the first time we've run the game, ie it is being installed, + // reset the user's release stream to "lazer". + // + // This ensures that if a user is trying to recover from a failed startup on an unstable release stream, + // the game doesn't immediately try and update them back to the release stream after starting up. + if (IsFirstRun) + LocalConfig.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer); + if (IsPackageManaged) return new NoActionUpdateManager(); diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index d275a8b7cf..3d7b5b48ba 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -28,8 +28,7 @@ namespace osu.Desktop private static LegacyTcpIpcProvider? legacyIpc; - // [DllImport("kernel32.dll", SetLastError = true)] - // private static extern bool SetDllDirectory(string lpPathName); + private static bool isFirstRun; [STAThread] public static void Main(string[] args) @@ -61,8 +60,6 @@ namespace osu.Desktop return; } } - - // SetDllDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "C:\\Users\\12456\\.nuget\\packages\\bass.asio\\1.3.1.2\\build\\native\\x64")); } // NVIDIA profiles are based on the executable name of a process. @@ -140,7 +137,12 @@ namespace osu.Desktop if (tournamentClient) host.Run(new TournamentGame()); else - host.Run(new OsuGameDesktop(args)); + { + host.Run(new OsuGameDesktop(args) + { + IsFirstRun = isFirstRun + }); + } } } @@ -182,6 +184,8 @@ namespace osu.Desktop var app = VelopackApp.Build(); + app.WithFirstRun(_ => isFirstRun = true); + if (OperatingSystem.IsWindows()) configureWindows(app); diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 33ff6c2b37..6f22fd5940 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -4,8 +4,10 @@ using System; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game; +using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; @@ -16,8 +18,8 @@ namespace osu.Desktop.Updater { public partial class VelopackUpdateManager : Game.Updater.UpdateManager { - private readonly UpdateManager updateManager; - private INotificationOverlay notificationOverlay = null!; + [Resolved] + private INotificationOverlay notificationOverlay { get; set; } = null!; [Resolved] private OsuGameBase game { get; set; } = null!; @@ -25,22 +27,32 @@ namespace osu.Desktop.Updater [Resolved] private ILocalUserPlayInfo? localUserInfo { get; set; } + [Resolved] + private OsuConfigManager osuConfigManager { get; set; } = null!; + private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying; + private readonly Bindable releaseStream = new Bindable(); + private UpdateManager? updateManager; private UpdateInfo? pendingUpdate; - public VelopackUpdateManager() + protected override void LoadComplete() { - updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions + // Used by the base implementation. + osuConfigManager.BindWith(OsuSetting.ReleaseStream, releaseStream); + releaseStream.BindValueChanged(_ => onReleaseStreamChanged(), true); + + base.LoadComplete(); + } + + private void onReleaseStreamChanged() + { + updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, releaseStream.Value == ReleaseStream.Tachyon), new UpdateOptions { AllowVersionDowngrade = true, }); - } - [BackgroundDependencyLoader] - private void load(INotificationOverlay notifications) - { - notificationOverlay = notifications; + Schedule(() => Task.Run(CheckForUpdateAsync)); } protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); @@ -76,6 +88,12 @@ namespace osu.Desktop.Updater return true; } + if (updateManager == null) + { + scheduleRecheck = true; + return false; + } + pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); // No update is available. We'll check again later. @@ -141,6 +159,9 @@ namespace osu.Desktop.Updater private async Task restartToApplyUpdate() { + if (updateManager == null) + return; + await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false); Schedule(() => game.AttemptExit()); } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index a9d18ba401..c5672cdd63 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { foreach (var holdNote in CreatedDrawables.SelectMany(d => d.ChildrenOfType())) { - ((Bindable)holdNote.IsHitting).Value = v; + ((Bindable)holdNote.IsHolding).Value = v; } }); } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs index 43c648a6dd..bf51584567 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRecording.cs @@ -46,8 +46,10 @@ namespace osu.Game.Rulesets.Mania.Tests public void TestRecording() { seekTo(0); - AddStep("press space", () => InputManager.Key(Key.Space)); - AddAssert("button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([ManiaAction.Key1]))); + AddStep("press space", () => InputManager.PressKey(Key.Space)); + seekTo(15); + AddStep("release space", () => InputManager.ReleaseKey(Key.Space)); + AddUntilStep("button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([ManiaAction.Key1]))); } private void seekTo(double time) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRewinding.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRewinding.cs new file mode 100644 index 0000000000..5216358a8b --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneReplayRewinding.cs @@ -0,0 +1,125 @@ +// Copyright (c) ppy Pty Ltd . 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 NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Replays; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public partial class TestSceneReplayRewinding : RateAdjustedBeatmapTestScene + { + private ReplayPlayer currentPlayer = null!; + + [Test] + public void TestRewindingToMiddleOfHoldNote() + { + Score score = null!; + + var beatmap = new ManiaBeatmap(new StageDefinition(4)) + { + HitObjects = + { + new HoldNote + { + StartTime = 500, + EndTime = 1500, + Column = 2 + } + } + }; + + AddStep(@"create replay", () => score = new Score + { + Replay = new Replay + { + Frames = + { + new ManiaReplayFrame(500, ManiaAction.Key3), + new ManiaReplayFrame(1500), + } + }, + ScoreInfo = new ScoreInfo() + }); + + AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(beatmap)); + AddStep(@"set ruleset", () => Ruleset.Value = beatmap.BeatmapInfo.Ruleset); + AddStep(@"push player", () => LoadScreen(currentPlayer = new ReplayPlayer(score))); + + AddUntilStep(@"wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep(@"wait for hold to be judged", () => currentPlayer.ChildrenOfType().Single().CurrentTime, () => Is.GreaterThan(1600)); + AddStep(@"seek to middle of hold note", () => currentPlayer.Seek(1000)); + AddUntilStep(@"wait for gameplay to complete", () => currentPlayer.GameplayState.HasCompleted); + AddAssert(@"no misses registered", () => currentPlayer.GameplayState.ScoreProcessor.Statistics.GetValueOrDefault(HitResult.Miss), () => Is.Zero); + + AddStep(@"exit player", () => currentPlayer.Exit()); + } + + [Test] + public void TestCorrectComboAccountingForConcurrentObjects() + { + Score score = null!; + + var beatmap = new ManiaBeatmap(new StageDefinition(4)) + { + HitObjects = + { + new Note + { + StartTime = 500, + Column = 0, + }, + new Note + { + StartTime = 500, + Column = 2, + }, + new HoldNote + { + StartTime = 1000, + EndTime = 1500, + Column = 1, + } + } + }; + + AddStep(@"create replay", () => score = new Score + { + Replay = new Replay + { + Frames = + { + new ManiaReplayFrame(500, ManiaAction.Key1, ManiaAction.Key3), + new ManiaReplayFrame(520), + new ManiaReplayFrame(1000, ManiaAction.Key2), + new ManiaReplayFrame(1500), + } + }, + ScoreInfo = new ScoreInfo() + }); + + AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(beatmap)); + AddStep(@"set ruleset", () => Ruleset.Value = beatmap.BeatmapInfo.Ruleset); + AddStep(@"push player", () => LoadScreen(currentPlayer = new ReplayPlayer(score))); + + AddUntilStep(@"wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep(@"wait for objects to be judged", () => currentPlayer.ChildrenOfType().Single().CurrentTime, () => Is.GreaterThan(1600)); + AddStep(@"stop gameplay", () => currentPlayer.ChildrenOfType().Single().Stop()); + AddStep(@"seek to start", () => currentPlayer.Seek(0)); + AddAssert(@"combo is 0", () => currentPlayer.GameplayState.ScoreProcessor.Combo.Value, () => Is.Zero); + + AddStep(@"exit player", () => currentPlayer.Exit()); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 1f99df5f8b..2f1478ce4f 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.ComponentModel; using osu.Framework.Configuration.Tracking; using osu.Game.Configuration; using osu.Game.Localisation; @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Configuration SetDefault(ManiaRulesetSetting.ScrollSpeed, 200, 1.0, 401.0, current_scroll_speed_precision); SetDefault(ManiaRulesetSetting.ScrollBaseSpeed, 500, 100, 1000, 1.0); SetDefault(ManiaRulesetSetting.ScrollTimePerSpeed, 5, 1.0, 40, 1.0); - SetDefault(ManiaRulesetSetting.ScrollStyle, ManiaScrollingStyle.ScrollTimeStyleFixed); + SetDefault(ManiaRulesetSetting.ScrollStyle, EzManiaScrollingStyle.ScrollTimeStyleFixed); SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); SetDefault(ManiaRulesetSetting.ScrollPerKeyMode, false); SetDefault(ManiaRulesetSetting.PerspectiveAngle, 90.0f, 30.0f, 90.0f); @@ -81,4 +82,18 @@ namespace osu.Game.Rulesets.Mania.Configuration TimingBasedNoteColouring, MobileLayout, } + + public enum EzManiaScrollingStyle + { + // [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ScrollingDirectionUp))] + [Description("40速 通配速度风格(不可用)")] + ScrollSpeedStyle, + + // [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ScrollingDirectionDown))] + [Description("ms值 恒定速度")] + ScrollTimeStyle, + + [Description("ms值 恒定时间")] + ScrollTimeStyleFixed, + } } diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgementResult.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgementResult.cs new file mode 100644 index 0000000000..d1453cb7ad --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgementResult.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects; + +namespace osu.Game.Rulesets.Mania.Judgements +{ + public class HoldNoteJudgementResult : JudgementResult + { + private Stack<(double time, bool holding)> holdingState { get; } = new Stack<(double, bool)>(); + + public HoldNoteJudgementResult(HoldNote hitObject, Judgement judgement) + : base(hitObject, judgement) + { + holdingState.Push((double.NegativeInfinity, false)); + } + + private (double time, bool holding) getLastReport(double currentTime) + { + while (holdingState.Peek().time > currentTime) + holdingState.Pop(); + + return holdingState.Peek(); + } + + public bool IsHolding(double currentTime) => getLastReport(currentTime).holding; + + public bool DroppedHoldAfter(double time) + { + foreach (var state in holdingState) + { + if (state.time >= time && !state.holding) + return true; + } + + return false; + } + + public void ReportHoldState(double currentTime, bool holding) + { + var lastReport = getLastReport(currentTime); + if (holding != lastReport.holding) + holdingState.Push((currentTime, holding)); + } + } +} diff --git a/osu.Game.Rulesets.Mania/LAsEZMania/EzStageDefinitionExtensions.cs b/osu.Game.Rulesets.Mania/LAsEZMania/EzStageDefinitionExtensions.cs index 8f30baaca8..5c1fc562c2 100644 --- a/osu.Game.Rulesets.Mania/LAsEZMania/EzStageDefinitionExtensions.cs +++ b/osu.Game.Rulesets.Mania/LAsEZMania/EzStageDefinitionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Mania.Beatmaps; using osuTK.Graphics; @@ -45,5 +46,129 @@ namespace osu.Game.Rulesets.Mania.LAsEZMania private static readonly Color4 colour_scratch = new Color4(20, 0, 0, 255); private static readonly Color4 colour_panel = new Color4(0, 20, 0, 255); private static readonly Color4 colour_alpha = new Color4(0, 0, 0, 0); + + // 颜色定义 + private static readonly Color4 colour_special = new Color4(206, 6, 3, 255); + + private static readonly Color4 colour_green = new Color4(100, 192, 92, 255); + private static readonly Color4 colour_red = new Color4(206, 6, 3, 255); + + private static readonly Color4 colour_withe = new Color4(222, 222, 222, 255); + private static readonly Color4 colour_blue = new Color4(55, 155, 255, 255); + + private const int total_colours = 3; + + private static readonly Color4 colour_cyan = new Color4(72, 198, 255, 255); + private static readonly Color4 colour_pink = new Color4(213, 35, 90, 255); + private static readonly Color4 colour_purple = new Color4(203, 60, 236, 255); + + public static Color4 GetColourForLayout(this StageDefinition stage, int columnIndex) + { + columnIndex %= stage.Columns; + + switch (stage.Columns) + { + case 4: + return columnIndex switch + { + 0 => colour_green, + 1 => colour_red, + 2 => colour_blue, + 3 => colour_cyan, + _ => throw new ArgumentOutOfRangeException() + }; + + case 5: + return columnIndex switch + { + 0 => colour_green, + 1 => colour_blue, + 2 => colour_red, + 3 => colour_cyan, + 4 => colour_purple, + _ => throw new ArgumentOutOfRangeException() + }; + + case 7: + return columnIndex switch + { + 1 or 5 => colour_withe, + 0 or 2 or 4 or 6 => colour_blue, + 3 => colour_green, + _ => throw new ArgumentOutOfRangeException() + }; + + case 8: + return columnIndex switch + { + 0 or 4 => colour_red, + 2 or 6 => colour_withe, + 1 or 3 or 5 or 7 => colour_blue, + _ => throw new ArgumentOutOfRangeException() + }; + + case 9: + return columnIndex switch + { + 0 or 6 or 7 => colour_red, + 2 or 4 => colour_withe, + 1 or 3 or 5 => colour_blue, + 8 => colour_green, + _ => throw new ArgumentOutOfRangeException() + }; + + case 10: + return columnIndex switch + { + 0 or 9 => colour_green, + 2 or 4 or 5 or 7 => colour_withe, + 1 or 3 or 6 or 8 => colour_blue, + _ => throw new ArgumentOutOfRangeException() + }; + + case 12: + return columnIndex switch + { + 0 or 11 => colour_red, + 1 or 3 or 5 or 6 or 8 or 10 => colour_withe, + 2 or 4 or 7 or 9 => colour_blue, + _ => throw new ArgumentOutOfRangeException() + }; + + case 14: + return columnIndex switch + { + 0 or 12 or 13 => colour_red, + 1 or 3 or 5 or 7 or 9 or 11 => colour_withe, + 2 or 4 or 8 or 10 => colour_blue, + 6 => colour_green, + _ => throw new ArgumentOutOfRangeException() + }; + + case 16: + return columnIndex switch + { + 0 or 6 or 7 or 8 or 9 or 15 => colour_red, + 1 or 3 or 5 or 10 or 12 or 14 => colour_withe, + 2 or 4 or 11 or 13 => colour_blue, + _ => throw new ArgumentOutOfRangeException() + }; + } + + // 后备逻辑保持不变 + if (stage.EzIsSpecialColumn(columnIndex)) + return colour_special; + + switch (columnIndex % total_colours) + { + case 0: return colour_cyan; + + case 1: return colour_pink; + + case 2: return colour_purple; + + default: throw new ArgumentOutOfRangeException(); + } + } } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 81f2692b2d..3ed035ad21 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -26,11 +26,12 @@ using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods.LAsMods; using osu.Game.Rulesets.Mania.Mods.YuLiangSSSMods; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Skinning.Ez2; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Skinning.Argon; using osu.Game.Rulesets.Mania.Skinning.Default; +using osu.Game.Rulesets.Mania.Skinning.Ez2; +using osu.Game.Rulesets.Mania.Skinning.EzStylePro; using osu.Game.Rulesets.Mania.Skinning.Legacy; using osu.Game.Rulesets.Mania.Skinning.SbI; using osu.Game.Rulesets.Mania.UI; @@ -83,10 +84,16 @@ namespace osu.Game.Rulesets.Mania return new ManiaArgonSkinTransformer(skin, beatmap); case Ez2Skin: - if (GlobalConfigStore.Config == null || GlobalConfigStore.EZConfig == null) + if (GlobalConfigStore.Config == null) throw new ArgumentNullException(nameof(GlobalConfigStore.Config)); - return new ManiaEz2SkinTransformer(skin, beatmap, GlobalConfigStore.Config, GlobalConfigStore.EZConfig); + return new ManiaEz2SkinTransformer(skin, beatmap, GlobalConfigStore.Config); + + case EzStyleProSkin: + if (GlobalConfigStore.Config == null) + throw new ArgumentNullException(nameof(GlobalConfigStore.Config)); + + return new ManiaEzStyleProSkinTransformer(skin, beatmap, GlobalConfigStore.Config); case SbISkin: return new ManiaSbISkinTransformer(skin, beatmap); @@ -304,7 +311,7 @@ namespace osu.Game.Rulesets.Mania return new Mod[] { new ManiaModEz2Settings(), - new ManiaHitModeConvertor(), + new EzManiaHitModeConvertor(), // new ManiaModJudgmentStyle(), new ManiaModNiceBPM(), new ManiaModSpaceBody(), @@ -375,8 +382,6 @@ namespace osu.Game.Rulesets.Mania { for (int i = 1; i <= MAX_STAGE_KEYS; i++) yield return (int)PlayfieldType.Single + i; - // for (int i = 2; i <= MAX_STAGE_KEYS * 2; i += 2) - // yield return (int)PlayfieldType.Dual + i; } } @@ -386,9 +391,6 @@ namespace osu.Game.Rulesets.Mania { case PlayfieldType.Single: return new SingleStageVariantGenerator(variant).GenerateMappings(); - - // case PlayfieldType.Dual: - // return new DualStageVariantGenerator(getDualStageKeyCount(variant)).GenerateMappings(); } return Array.Empty(); @@ -400,21 +402,9 @@ namespace osu.Game.Rulesets.Mania { default: return $"{variant}K"; - - // case PlayfieldType.Dual: - // { - // int keys = getDualStageKeyCount(variant); - // return $"{keys}K + {keys}K"; - // } } } - // /// - // /// Finds the number of keys for each stage in a variant. - // /// - // /// The variant. - // private int getDualStageKeyCount(int variant) => (variant - (int)PlayfieldType.Dual) / 2; - /// /// Finds the that corresponds to a variant value. /// @@ -442,25 +432,6 @@ namespace osu.Game.Rulesets.Mania }; } - // public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] - // { - // new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) - // { - // RelativeSizeAxes = Axes.X, - // AutoSizeAxes = Axes.Y - // }), - // new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) //判定偏移 - // { - // RelativeSizeAxes = Axes.X, - // Height = 200 - // }, true), - // new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[] - // { - // new AverageHitError(score.HitEvents), - // new UnstableRate(score.HitEvents) - // }), true), - // }; - public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) //单轨道判定分布 { var hitEventsByColumn = score.HitEvents diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 15e28cdc3c..9dcc49747e 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -29,33 +29,15 @@ namespace osu.Game.Rulesets.Mania Children = new Drawable[] { - // new SettingsSlider - // { - // LabelText = "Column Width(Not Active)", - // Current = config.GetBindable(ManiaRulesetSetting.ColumnWidth), - // KeyboardStep = 1, - // }, - // new SettingsSlider - // { - // LabelText = "Special Column Width Factor(Not Active)", - // Current = config.GetBindable(ManiaRulesetSetting.SpecialFactor), - // KeyboardStep = 1, - // }, - // new SettingsEnumDropdown - // { - // ClassicDefault = MUGHitMode.EZ2AC, - // LabelText = "MUG HitMode(No Active)", - // Current = config.GetBindable(ManiaRulesetSetting.HitMode) - // }, new SettingsEnumDropdown { LabelText = RulesetSettingsStrings.ScrollingDirection, Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection) }, - new SettingsEnumDropdown + new SettingsEnumDropdown { LabelText = "Scrolling style", - Current = config.GetBindable(ManiaRulesetSetting.ScrollStyle) + Current = config.GetBindable(ManiaRulesetSetting.ScrollStyle) }, new SettingsSlider { diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs index b5490aa950..143a5f1bdc 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNoRelease.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Mods protected override void CheckForResult(bool userTriggered, double timeOffset) { // apply perfect once the tail is reached - if (HoldNote.HoldStartTime != null && timeOffset >= 0) + if (HoldNote.IsHolding.Value && timeOffset >= 0) ApplyResult(GetCappedResult(HitResult.Perfect)); else base.CheckForResult(userTriggered, timeOffset); diff --git a/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModNoLNJudgement.cs b/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModNoLNJudgement.cs index d26cf897c7..3716c18302 100644 --- a/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModNoLNJudgement.cs +++ b/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModNoLNJudgement.cs @@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.Mods.YuLiangSSSMods { public new bool HasHoldBreak => false; - internal override void TriggerResult(bool hit) + internal new void TriggerResult(bool hit) { if (AllJudged) return; @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Mania.Mods.YuLiangSSSMods return; } - if (HoldNote.IsHitting.Value) + if (HoldNote.IsHolding.Value) { return; } diff --git a/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModReleaseAdjust.cs b/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModReleaseAdjust.cs index 87237d8a91..c4fdb26d36 100644 --- a/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModReleaseAdjust.cs +++ b/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModReleaseAdjust.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.Mods protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (HoldNote.HoldStartTime != null && Math.Abs(timeOffset) <= ReleaseOffset) + if (Math.Abs(timeOffset) <= ReleaseOffset) ApplyResult(GetCappedResult(HitResult.Perfect)); else base.CheckForResult(userTriggered, timeOffset); diff --git a/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModRemedy.cs b/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModRemedy.cs index d570bc56c3..cf9d5ba55d 100644 --- a/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModRemedy.cs +++ b/osu.Game.Rulesets.Mania/Mods/YuLiangSSSMods/ManiaModRemedy.cs @@ -256,27 +256,27 @@ namespace osu.Game.Rulesets.Mania.Mods.YuLiangSSSMods protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (HoldNote.HoldStartTime != null && userTriggered && RemedyGreat > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Perfect))) + if (HoldNote.Head.IsHit && userTriggered && RemedyGreat > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Perfect))) { RemedyGreat--; ApplyResult(GetCappedResult(HitResult.Perfect)); } - else if (HoldNote.HoldStartTime != null && userTriggered && RemedyGood > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Great))) + else if (HoldNote.Head.IsHit && userTriggered && RemedyGood > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Great))) { RemedyGood--; ApplyResult(GetCappedResult(HitResult.Great)); } - else if (HoldNote.HoldStartTime != null && userTriggered && RemedyOk > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Good))) + else if (HoldNote.Head.IsHit && userTriggered && RemedyOk > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Good))) { RemedyOk--; ApplyResult(GetCappedResult(HitResult.Good)); } - else if (HoldNote.HoldStartTime != null && userTriggered && RemedyMeh > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Ok))) + else if (HoldNote.Head.IsHit && userTriggered && RemedyMeh > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Ok))) { RemedyMeh--; ApplyResult(GetCappedResult(HitResult.Ok)); } - else if (HoldNote.HoldStartTime != null && userTriggered && RemedyMiss > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Meh)) + else if (HoldNote.Head.IsHit && userTriggered && RemedyMiss > 0 && Math.Abs(timeOffset) > Math.Abs(HitWindows.WindowFor(HitResult.Meh)) && Math.Abs(timeOffset) <= Math.Abs(HitWindows.WindowFor(HitResult.Miss))) { RemedyMiss--; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 9c56f0473c..6c607886ae 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Audio; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -29,9 +31,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { public override bool DisplayResult => false; - public IBindable IsHitting => isHitting; + public IBindable IsHolding => isHolding; - private readonly Bindable isHitting = new Bindable(); + private readonly Bindable isHolding = new Bindable(); public DrawableHoldNoteHead Head => headContainer.Child; public DrawableHoldNoteTail Tail => tailContainer.Child; @@ -55,16 +57,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private SkinnableDrawable bodyPiece; - /// - /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. - /// - public double? HoldStartTime { get; private set; } - - /// - /// Used to decide whether to visually clamp the hold note to the judgement line. - /// - private double? releaseTime; - public DrawableHoldNote() : this(null) { @@ -126,7 +118,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.LoadComplete(); - isHitting.BindValueChanged(updateSlidingSample, true); + isHolding.BindValueChanged(updateSlidingSample, true); } protected override void OnApply() @@ -134,8 +126,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.OnApply(); sizingContainer.Size = Vector2.One; - HoldStartTime = null; - releaseTime = null; } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -214,11 +204,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.Update(); - if (Time.Current < releaseTime) - releaseTime = null; - - if (Time.Current < HoldStartTime) - endHold(); + isHolding.Value = Result.IsHolding(Time.Current); // Pad the full size container so its contents (i.e. the masking container) reach under the tail. // This is required for the tail to not be masked away, since it lies outside the bounds of the hold note. @@ -249,7 +235,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // // As per stable, this should not apply for early hits, waiting until the object starts to touch the // judgement area first. - if (Head.IsHit && releaseTime == null && DrawHeight > 0) + if (Head.IsHit && !Result.DroppedHoldAfter(HitObject.StartTime) && DrawHeight > 0) { // How far past the hit target this hold note is. float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y; @@ -260,6 +246,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables sizingContainer.Height = 1; } + protected override JudgementResult CreateResult(Judgement judgement) => new HoldNoteJudgementResult(HitObject, judgement); + + public new HoldNoteJudgementResult Result => (HoldNoteJudgementResult)base.Result; + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) @@ -274,7 +264,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Body.TriggerResult(Tail.IsHit); // Important that this is always called when a result is applied. - endHold(); + Result.ReportHoldState(Time.Current, false); } } @@ -283,7 +273,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.MissForcefully(); // Important that this is always called when a result is applied. - endHold(); + Result.ReportHoldState(Time.Current, false); } public bool OnPressed(KeyBindingPressEvent e) @@ -317,8 +307,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (timeOffset < -Head.HitObject.HitWindows.WindowFor(HitResult.Miss)) return; - HoldStartTime = Time.Current; - isHitting.Value = true; + Result.ReportHoldState(Time.Current, true); } public void OnReleased(KeyBindingReleaseEvent e) @@ -337,22 +326,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // the user has released too early (before the tail). // // In such a case, we want to record this against the DrawableHoldNoteBody. - if (HoldStartTime != null) + if (isHolding.Value) { Tail.UpdateResult(); Body.TriggerResult(Tail.IsHit); - endHold(); - releaseTime = Time.Current; + Result.ReportHoldState(Time.Current, false); } } - private void endHold() - { - HoldStartTime = null; - isHitting.Value = false; - } - protected override void LoadSamples() { // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs index 784ac96230..6259033235 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { } - internal virtual void TriggerResult(bool hit) + internal void TriggerResult(bool hit) { if (AllJudged) return; diff --git a/osu.Game.Rulesets.Mania/Objects/Ez2AcHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Ez2AcHoldNote.cs index d8f6eda2bc..ed3a5ac046 100644 --- a/osu.Game.Rulesets.Mania/Objects/Ez2AcHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Ez2AcHoldNote.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Objects public partial class Ez2AcDrawableHoldNoteBody : DrawableHoldNoteBody { - internal override void TriggerResult(bool hit) + internal new void TriggerResult(bool hit) { if (AllJudged) return; @@ -91,13 +91,13 @@ namespace osu.Game.Rulesets.Mania.Objects protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (HoldNote.IsHitting.Value && timeOffset >= 0) + if (HoldNote.IsHolding.Value && timeOffset >= 0) { ApplyResult(GetCappedResult(HitResult.Perfect)); return; } - if (!HoldNote.IsHitting.Value && timeOffset < 0) + if (!HoldNote.IsHolding.Value && timeOffset < 0) { ApplyResult(GetCappedResult(HitResult.ComboBreak)); return; diff --git a/osu.Game.Rulesets.Mania/Objects/MalodyHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/MalodyHoldNote.cs index 2602cefa71..8bca6f728a 100644 --- a/osu.Game.Rulesets.Mania/Objects/MalodyHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/MalodyHoldNote.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Objects { public new bool HasHoldBreak => false; - internal override void TriggerResult(bool hit) + internal new void TriggerResult(bool hit) { if (AllJudged) return; @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Objects return; } - if (HoldNote.IsHitting.Value) + if (HoldNote.IsHolding.Value) { return; } diff --git a/osu.Game.Rulesets.Mania/Objects/NoJudgmentHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/NoJudgmentHoldNote.cs index efb8d3a2a2..e7871ebe7c 100644 --- a/osu.Game.Rulesets.Mania/Objects/NoJudgmentHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/NoJudgmentHoldNote.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.Objects protected override void CheckForResult(bool userTriggered, double timeOffset) { // apply perfect once the tail is reached - if (HoldNote.HoldStartTime != null && timeOffset >= 0) + if (HoldNote.Head.IsHit && timeOffset >= 0) ApplyResult(GetCappedResult(HitResult.Perfect)); else base.CheckForResult(userTriggered, timeOffset); diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitModeConvertor.cs b/osu.Game.Rulesets.Mania/Scoring/EzManiaHitModeConvertor.cs similarity index 98% rename from osu.Game.Rulesets.Mania/Scoring/ManiaHitModeConvertor.cs rename to osu.Game.Rulesets.Mania/Scoring/EzManiaHitModeConvertor.cs index 92d08ce987..4532631e5a 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitModeConvertor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/EzManiaHitModeConvertor.cs @@ -22,7 +22,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Scoring { - public class ManiaHitModeConvertor : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableRuleset + public class EzManiaHitModeConvertor : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableRuleset { public override string Name => "Mania Hit Mode"; public override LocalisableString Description => "LaMod: To Use Setting"; @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Scoring // private readonly BindableDouble configColumnWidth = new BindableDouble(); // private readonly BindableDouble configSpecialFactor = new BindableDouble(); - public ManiaHitModeConvertor() + public EzManiaHitModeConvertor() { // if (GlobalConfigStore.Config != null) // HitMode.Value = GlobalConfigStore.Config.Get(OsuSetting.HitMode); diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitTimingInfo.cs b/osu.Game.Rulesets.Mania/Scoring/EzManiaHitTimingInfo.cs similarity index 79% rename from osu.Game.Rulesets.Mania/Scoring/ManiaHitTimingInfo.cs rename to osu.Game.Rulesets.Mania/Scoring/EzManiaHitTimingInfo.cs index 5085f5ec54..37fab4e9b2 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitTimingInfo.cs +++ b/osu.Game.Rulesets.Mania/Scoring/EzManiaHitTimingInfo.cs @@ -5,12 +5,12 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { - public class ManiaHitTimingInfo + public class EzManiaHitTimingInfo { public double HitTime { get; set; } public HitResult Result { get; set; } - public ManiaHitTimingInfo(double hitTime, HitResult result) + public EzManiaHitTimingInfo(double hitTime, HitResult result) { HitTime = hitTime; Result = result; diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 30cb1ab2da..4906fadb39 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Mania.Scoring { public partial class ManiaScoreProcessor : ScoreProcessor { - public List HitTimings { get; private set; } = new List(); + public List HitTimings { get; private set; } = new List(); public void AddHitTiming(double hitTime, HitResult result) { - HitTimings.Add(new ManiaHitTimingInfo(hitTime, result)); + HitTimings.Add(new EzManiaHitTimingInfo(hitTime, result)); } public double CalculateScoreWithParameters(double comboProgress, double accuracyProgress, double bonusPortion, Dictionary customHitProportionScore) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs index 57fa1c10ae..b0a14d27ab 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon AccentColour.BindTo(holdNote.AccentColour); hittingLayer.AccentColour.BindTo(holdNote.AccentColour); - ((IBindable)hittingLayer.IsHitting).BindTo(holdNote.IsHitting); + ((IBindable)hittingLayer.IsHitting).BindTo(holdNote.IsHolding); } AccentColour.BindValueChanged(colour => diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs index efd7f4f280..3c69a05003 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon hittingLayer.AccentColour.BindTo(holdNoteTail.HoldNote.AccentColour); hittingLayer.IsHitting.UnbindBindings(); - ((IBindable)hittingLayer.IsHitting).BindTo(holdNoteTail.HoldNote.IsHitting); + ((IBindable)hittingLayer.IsHitting).BindTo(holdNoteTail.HoldNote.IsHolding); } private void onDirectionChanged(ValueChangedEvent direction) diff --git a/osu.Game.Rulesets.Mania/Skinning/BindableExtensions.cs b/osu.Game.Rulesets.Mania/Skinning/BindableExtensions.cs deleted file mode 100644 index b78b3e8bfb..0000000000 --- a/osu.Game.Rulesets.Mania/Skinning/BindableExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using osu.Framework.Bindables; - -namespace osu.Game.Rulesets.Mania.Skinning -{ - public static class BindableExtensions - { - public static Bindable ConvertToFloatBindable(this IBindable doubleBindable) - { - var floatBindable = new Bindable(); - // 将 double 绑定到 float 绑定上,初始绑定时转换一次,并且之后值更新时同步 - doubleBindable.BindValueChanged(e => floatBindable.Value = (float)e.NewValue, true); - return floatBindable; - } - } -} diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs index 9f5ee0846f..67792e9027 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default var holdNote = (DrawableHoldNote)drawableObject; AccentColour.BindTo(drawableObject.AccentColour); - IsHitting.BindTo(holdNote.IsHitting); + IsHitting.BindTo(holdNote.IsHolding); } AccentColour.BindValueChanged(onAccentChanged, true); diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboCounter.cs index c588b65d1a..a0952c97ee 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboCounter.cs @@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD { public partial class EzComComboCounter : ComboCounter { - [SettingSource("Font", "Font", SettingControlType = typeof(EzEnumListSelector))] - public Bindable NameDropdown { get; } = new Bindable((OffsetNumberName)4); + [SettingSource("Font", "Font", SettingControlType = typeof(EzSelectorEnumList))] + public Bindable NameDropdown { get; } = new Bindable((EzSelectorNameSet)4); [SettingSource("Effect Type", "Effect Type")] public Bindable Effect { get; } = new Bindable(EffectType.Scale); diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs index 3cf0784f46..109d93c9f8 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD { public partial class EzComComboSprite : HitErrorMeter { - [SettingSource("Combo Text Font", "Combo Text Font", SettingControlType = typeof(EzEnumListSelector))] - public Bindable NameDropdown { get; } = new Bindable((OffsetNumberName)4); + [SettingSource("Combo Text Font", "Combo Text Font", SettingControlType = typeof(EzSelectorEnumList))] + public Bindable NameDropdown { get; } = new Bindable((EzSelectorNameSet)4); [SettingSource("Effect Type", "Effect Type")] public Bindable Effect { get; } = new Bindable(EffectType.Scale); diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTiming.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTiming.cs index 328e26d006..64c266a61c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTiming.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTiming.cs @@ -17,11 +17,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD [Cached] public partial class EzComHitTiming : HitErrorMeter { - [SettingSource("Offset Number Font", "Offset Number Font", SettingControlType = typeof(EzEnumListSelector))] - public Bindable NumberNameDropdown { get; } = new Bindable((OffsetNumberName)28); + [SettingSource("Offset Number Font", "Offset Number Font", SettingControlType = typeof(EzSelectorEnumList))] + public Bindable NumberNameDropdown { get; } = new Bindable((EzSelectorNameSet)28); [SettingSource("Offset Text Font", "Offset Text Font", SettingControlType = typeof(OffsetTextNameSelector))] - public Bindable TextNameDropdown { get; } = new Bindable((OffsetNumberName)28); + public Bindable TextNameDropdown { get; } = new Bindable((EzSelectorNameSet)28); [SettingSource("AloneShow", "Show only Early or: Late separately")] public Bindable AloneShow { get; } = new Bindable(AloneShowMenu.None); @@ -278,7 +278,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD None, } - public partial class OffsetTextNameSelector : EzEnumListSelector + public partial class OffsetTextNameSelector : EzSelectorEnumList { } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTimingColumns.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTimingColumns.cs index 6e4c1f89f3..70f719d3fa 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTimingColumns.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTimingColumns.cs @@ -240,7 +240,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD private float getRelativeJudgementPosition(double value) { float moveRange = MoveHeight.Value; - return (float)Math.Clamp((value / HitWindows.WindowFor(HitResult.Miss)) * moveRange, -moveRange, moveRange); + double missWindow = HitWindows.WindowFor(HitResult.Miss); + + if (moveRange == 0 || missWindow == 0) + return 0; + + float pos = (float)(value / (missWindow * moveRange)); + return Math.Clamp(pos, -moveRange, moveRange); } private int getColumnIndex(JudgementResult judgement) diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HitExplosion.cs index 0d885927bb..976479939a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HitExplosion.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 { Type = EdgeEffectType.Glow, Colour = colour.NewValue, - Roundness = Ez2NotePiece.NoteHeight, + Roundness = NoteHeight, Radius = 50, }; }, true); diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HoldBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HoldBodyPiece.cs index a008fd1459..08565f5534 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HoldBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HoldBodyPiece.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 AccentColour.BindTo(holdNote.AccentColour); hittingLayer.AccentColour.BindTo(holdNote.AccentColour); - ((IBindable)hittingLayer.IsHitting).BindTo(holdNote.IsHitting); + ((IBindable)hittingLayer.IsHitting).BindTo(holdNote.IsHolding); } AccentColour.BindValueChanged(colour => diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/ManiaEz2SkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/ManiaEz2SkinTransformer.cs index 6dd23bad42..7c91d22ebf 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/ManiaEz2SkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/ManiaEz2SkinTransformer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -12,9 +11,9 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.LAsEZMania; using osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; +using osu.Game.Skinning.Components; using osuTK; using osuTK.Graphics; @@ -35,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 private float columnWidth { get; set; } - public ManiaEz2SkinTransformer(ISkin skin, IBeatmap beatmap, OsuConfigManager config, EzSkinSettings ezSkinSettings) + public ManiaEz2SkinTransformer(ISkin skin, IBeatmap beatmap, OsuConfigManager config) : base(skin) { this.beatmap = (ManiaBeatmap)beatmap; @@ -190,9 +189,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 // return new Ez2JudgementPiece(resultComponent.Component); return Drawable.Empty(); - // return new DefaultSkinComponentsContainer(container => - // { - // }); + // return new DefaultSkinComponentsContainer(container => + // { + // }); case ManiaSkinComponentLookup maniaComponent: switch (maniaComponent.Component) @@ -206,23 +205,23 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 return new Ez2ColumnBackground(); - case ManiaSkinComponents.HoldNoteBody: - return new Ez2HoldBodyPiece(); - - case ManiaSkinComponents.HoldNoteTail: - return new Ez2HoldNoteTailPiece(); - - case ManiaSkinComponents.HoldNoteHead: - return new Ez2HoldNoteHeadPiece(); - - case ManiaSkinComponents.Note: - return new Ez2NotePiece(); + case ManiaSkinComponents.KeyArea: + return new Ez2KeyArea(); case ManiaSkinComponents.HitTarget: return new Ez2HitTarget(); - case ManiaSkinComponents.KeyArea: - return new Ez2KeyArea(); + case ManiaSkinComponents.Note: + return new Ez2NotePiece(); + + case ManiaSkinComponents.HoldNoteTail: + return new Ez2HoldNoteTailPiece(); + + case ManiaSkinComponents.HoldNoteBody: + return new Ez2HoldBodyPiece(); + + case ManiaSkinComponents.HoldNoteHead: + return new Ez2HoldNoteHeadPiece(); case ManiaSkinComponents.HitExplosion: return new Ez2HitExplosion(); @@ -251,6 +250,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 switch (maniaLookup.Lookup) { + case LegacyManiaSkinConfigurationLookups.ColumnWidth: + return SkinUtils.As(new Bindable(columnWidth)); + + case LegacyManiaSkinConfigurationLookups.HitPosition: + return SkinUtils.As(new Bindable(hitPosition)); + + case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: + + var colour = stage.GetColourForLayout(columnIndex); + + return SkinUtils.As(new Bindable(colour)); + case LegacyManiaSkinConfigurationLookups.BarLineHeight: return SkinUtils.As(new Bindable(1)); @@ -263,145 +274,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 case LegacyManiaSkinConfigurationLookups.StagePaddingTop: return SkinUtils.As(new Bindable(0)); - - case LegacyManiaSkinConfigurationLookups.HitPosition: - return SkinUtils.As(new Bindable(hitPosition)); - - case LegacyManiaSkinConfigurationLookups.ColumnWidth: - return SkinUtils.As(new Bindable(columnWidth)); - - case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: - - var colour = getColourForLayout(columnIndex, stage); - - return SkinUtils.As(new Bindable(colour)); } } return base.GetConfig(lookup); } - - private static readonly Color4 colour_special = new Color4(206, 6, 3, 255); - - private static readonly Color4 colour_green = new Color4(100, 192, 92, 255); - private static readonly Color4 colour_red = new Color4(206, 6, 3, 255); - - private static readonly Color4 colour_withe = new Color4(222, 222, 222, 255); - private static readonly Color4 colour_blue = new Color4(55, 155, 255, 255); - - private const int total_colours = 3; - - private static readonly Color4 colour_cyan = new Color4(72, 198, 255, 255); - private static readonly Color4 colour_pink = new Color4(213, 35, 90, 255); - private static readonly Color4 colour_purple = new Color4(203, 60, 236, 255); - - private Color4 getColourForLayout(int columnIndex, StageDefinition stage) - { - columnIndex %= stage.Columns; - - switch (stage.Columns) - { - case 4: - return columnIndex switch - { - 0 => colour_green, - 1 => colour_red, - 2 => colour_blue, - 3 => colour_cyan, - _ => throw new ArgumentOutOfRangeException() - }; - - case 5: - return columnIndex switch - { - 0 => colour_green, - 1 => colour_blue, - 2 => colour_red, - 3 => colour_cyan, - 4 => colour_purple, - _ => throw new ArgumentOutOfRangeException() - }; - - case 7: - return columnIndex switch - { - 1 or 5 => colour_withe, - 0 or 2 or 4 or 6 => colour_blue, - 3 => colour_green, - _ => throw new ArgumentOutOfRangeException() - }; - - case 8: - return columnIndex switch - { - 0 or 4 => colour_red, - 2 or 6 => colour_withe, - 1 or 3 or 5 or 7 => colour_blue, - _ => throw new ArgumentOutOfRangeException() - }; - - case 9: - return columnIndex switch - { - 0 or 6 or 7 => colour_red, - 2 or 4 => colour_withe, - 1 or 3 or 5 => colour_blue, - 8 => colour_green, - _ => throw new ArgumentOutOfRangeException() - }; - - case 10: - return columnIndex switch - { - 0 or 9 => colour_green, - 2 or 4 or 5 or 7 => colour_withe, - 1 or 3 or 6 or 8 => colour_blue, - _ => throw new ArgumentOutOfRangeException() - }; - - case 12: - return columnIndex switch - { - 0 or 11 => colour_red, - 1 or 3 or 5 or 6 or 8 or 10 => colour_withe, - 2 or 4 or 7 or 9 => colour_blue, - _ => throw new ArgumentOutOfRangeException() - }; - - case 14: - return columnIndex switch - { - 0 or 12 or 13 => colour_red, - 1 or 3 or 5 or 7 or 9 or 11 => colour_withe, - 2 or 4 or 8 or 10 => colour_blue, - 6 => colour_green, - _ => throw new ArgumentOutOfRangeException() - }; - - case 16: - return columnIndex switch - { - 0 or 6 or 7 or 8 or 9 or 15 => colour_red, - 1 or 3 or 5 or 10 or 12 or 14 => colour_withe, - 2 or 4 or 11 or 13 => colour_blue, - _ => throw new ArgumentOutOfRangeException() - }; - } - - // 后备逻辑保持不变 - if (stage.EzIsSpecialColumn(columnIndex)) - return colour_special; - - switch (columnIndex % total_colours) - { - case 0: return colour_cyan; - - case 1: return colour_pink; - - case 2: return colour_purple; - - default: throw new ArgumentOutOfRangeException(); - } - } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 6de0752671..606f83201b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy var wrapMode = bodyStyle == LegacyNoteBodyStyle.Stretch ? WrapMode.ClampToEdge : WrapMode.Repeat; direction.BindTo(scrollingInfo.Direction); - isHitting.BindTo(holdNote.IsHitting); + isHitting.BindTo(holdNote.IsHolding); bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30)?.With(d => { diff --git a/osu.Game.Rulesets.Mania/Skinning/SbI/ManiaSbISkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/SbI/ManiaSbISkinTransformer.cs index 443611be82..75ed6947b9 100644 --- a/osu.Game.Rulesets.Mania/Skinning/SbI/ManiaSbISkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/SbI/ManiaSbISkinTransformer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI combo1.Origin = Anchor.Centre; combo1.Y = 200; combo1.Effect.Value = EffectType.None; - combo1.NameDropdown.Value = (OffsetNumberName)43; + combo1.NameDropdown.Value = (EzSelectorNameSet)43; } var hitErrorMeter = container.OfType().FirstOrDefault(); diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index dd4519ea0d..0400a22906 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; private readonly Bindable configDirection = new Bindable(); - private readonly Bindable scrollingStyle = new Bindable(); + private readonly Bindable scrollingStyle = new Bindable(); private readonly BindableDouble configBaseMs = new BindableDouble(); private readonly BindableDouble configTimePerSpeed = new BindableDouble(); private readonly BindableDouble configScrollSpeed = new BindableDouble(); @@ -186,13 +186,13 @@ namespace osu.Game.Rulesets.Mania.UI switch (scrollingStyle.Value) { - case ManiaScrollingStyle.ScrollSpeedStyle: - case ManiaScrollingStyle.ScrollTimeStyle: + case EzManiaScrollingStyle.ScrollSpeedStyle: + case EzManiaScrollingStyle.ScrollTimeStyle: // Preserve the scroll speed as the scroll length varies from changes to the hit position. scale = lengthToHitPosition / length_to_default_hit_position; break; - case ManiaScrollingStyle.ScrollTimeStyleFixed: + case EzManiaScrollingStyle.ScrollTimeStyleFixed: // Ensure the travel time from the top of the screen to the hit position remains constant. scale = length_to_default_hit_position / lengthToHitPosition; break; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingStyle.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingStyle.cs deleted file mode 100644 index 5ea14292b5..0000000000 --- a/osu.Game.Rulesets.Mania/UI/ManiaScrollingStyle.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.ComponentModel; - -namespace osu.Game.Rulesets.Mania.UI -{ - public enum ManiaScrollingStyle - { - // [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ScrollingDirectionUp))] - [Description("40速 通配速度风格(不可用)")] - ScrollSpeedStyle, - - // [LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.ScrollingDirectionDown))] - [Description("ms值 恒定速度")] - ScrollTimeStyle, - - [Description("ms值 恒定时间")] - ScrollTimeStyleFixed, - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs index 66a60e3542..170d9830f2 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModStrictTracking.cs @@ -49,5 +49,59 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods }, PassCondition = () => Player.ScoreProcessor.Combo.Value == 2 }); + + [Test] + public void TestRewind() + { + bool seekedBack = false; + bool missRecorded = false; + + CreateModTest(new ModTestData + { + Mod = new OsuModStrictTracking(), + Autoplay = false, + CreateBeatmap = () => new Beatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 1000, + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(0, 100)) + } + } + } + } + }, + ReplayFrames = new List + { + new OsuReplayFrame(0, new Vector2(100, 0)), + new OsuReplayFrame(1000, new Vector2(100, 0)), + new OsuReplayFrame(1050, new Vector2()), + new OsuReplayFrame(1100, new Vector2(), OsuAction.LeftButton), + new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton), + new OsuReplayFrame(1751, new Vector2(0, 100)), + }, + PassCondition = () => seekedBack && !missRecorded, + }); + AddStep("subscribe to new judgements", () => Player.ScoreProcessor.NewJudgement += j => + { + if (!j.IsHit) + missRecorded = true; + }); + AddUntilStep("wait for gameplay completion", () => Player.GameplayState.HasCompleted); + AddAssert("no misses", () => missRecorded, () => Is.False); + AddStep("seek back", () => + { + Player.GameplayClockContainer.Stop(); + Player.Seek(1040); + seekedBack = true; + }); + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 184938ceda..06ab6e496f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Framework.Timing; using osu.Game.Replays; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Replays; @@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests [Cached] private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo); + private readonly StopwatchClock clock = new StopwatchClock(); + [SetUpSteps] public void SetUpSteps() { @@ -35,7 +38,10 @@ namespace osu.Game.Rulesets.Osu.Tests { new OsuPlayfieldAdjustmentContainer { - Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()), + Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()) + { + Clock = new FramedClock(clock) + }, }, settings = new ReplayAnalysisSettings(config), }; @@ -55,11 +61,23 @@ namespace osu.Game.Rulesets.Osu.Tests settings.ShowAimMarkers.Value = true; settings.ShowCursorPath.Value = true; }); + AddToggleStep("toggle pause", running => + { + if (running) + clock.Stop(); + else + clock.Start(); + }); } [Test] public void TestHitMarkers() { + AddStep("stop at 2000", () => + { + clock.Stop(); + clock.Seek(2000); + }); AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true); AddUntilStep("hit markers visible", () => analysisContainer.HitMarkersVisible); AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false); @@ -69,6 +87,11 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestAimMarker() { + AddStep("stop at 2000", () => + { + clock.Stop(); + clock.Seek(2000); + }); AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true); AddUntilStep("aim markers visible", () => analysisContainer.AimMarkersVisible); AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false); @@ -78,6 +101,11 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestAimLines() { + AddStep("stop at 2000", () => + { + clock.Stop(); + clock.Seek(2000); + }); AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true); AddUntilStep("aim lines visible", () => analysisContainer.AimLinesVisible); AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false); @@ -87,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests private Replay fabricateReplay() { var frames = new List(); - var random = new Random(); + var random = new Random(20250522); int posX = 250; int posY = 250; @@ -109,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Tests frames.Add(new OsuReplayFrame { - Time = Time.Current + i * 15, + Time = i * 15, Position = new Vector2(posX, posY), Actions = actions.ToList(), }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs index d163e8a1b4..6b867a7729 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneReplayRecording.cs @@ -58,17 +58,23 @@ namespace osu.Game.Rulesets.Osu.Tests { seekTo(0); AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single())); - AddStep("press X", () => InputManager.Key(Key.X)); + AddStep("press X", () => InputManager.PressKey(Key.X)); + seekTo(15); + AddStep("release X", () => InputManager.ReleaseKey(Key.X)); AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton]))); seekTo(5000); AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single())); - AddStep("press Z", () => InputManager.Key(Key.Z)); + AddStep("press Z", () => InputManager.PressKey(Key.Z)); + seekTo(5015); + AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); AddAssert("left button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([OsuAction.LeftButton]))); seekTo(10000); AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single())); - AddStep("press C", () => InputManager.Key(Key.C)); + AddStep("press C", () => InputManager.PressKey(Key.C)); + seekTo(10015); + AddStep("release C", () => InputManager.ReleaseKey(Key.C)); AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke]))); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index c28226fcf4..e4f8ee5b6d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -254,7 +254,7 @@ namespace osu.Game.Rulesets.Osu.Edit [CanBeNull] public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null) { - if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null) + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid?.IsLoaded != true) return null; var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 7d2fd628f6..5ee8814b5a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Osu.Mods { @@ -39,6 +40,9 @@ namespace osu.Game.Rulesets.Osu.Mods if (slider.Time.Current < slider.HitObject.StartTime) return; + if ((slider.Clock as IGameplayClock)?.IsRewinding == true) + return; + var tail = slider.NestedHitObjects.OfType().First(); if (!tail.Judged) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs index 14a1fbfa99..bd79bbe8cf 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneReplayRecording.cs @@ -40,19 +40,27 @@ namespace osu.Game.Rulesets.Taiko.Tests public void TestRecording() { seekTo(0); - AddStep("press D", () => InputManager.Key(Key.D)); + AddStep("press D", () => InputManager.PressKey(Key.D)); + seekTo(15); + AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddAssert("left rim press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftRim]))); seekTo(5000); - AddStep("press F", () => InputManager.Key(Key.F)); + AddStep("press F", () => InputManager.PressKey(Key.F)); + seekTo(5015); + AddStep("release F", () => InputManager.ReleaseKey(Key.F)); AddAssert("left centre press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([TaikoAction.LeftCentre]))); seekTo(10000); - AddStep("press J", () => InputManager.Key(Key.J)); + AddStep("press J", () => InputManager.PressKey(Key.J)); + seekTo(10015); + AddStep("release J", () => InputManager.ReleaseKey(Key.J)); AddAssert("right centre press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([TaikoAction.RightCentre]))); - seekTo(10000); - AddStep("press K", () => InputManager.Key(Key.K)); + seekTo(15000); + AddStep("press K", () => InputManager.PressKey(Key.K)); + seekTo(15015); + AddStep("release K", () => InputManager.ReleaseKey(Key.K)); AddAssert("right rim press recorded to replay", () => Player.Score.Replay.Frames.OfType().Any(f => f.Actions.SequenceEqual([TaikoAction.RightRim]))); } diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 1647fbee42..f45422e0c4 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -421,6 +421,65 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH)); } + [Test] + public void TestComboAccounting([Values] bool shuffleResults) + { + var testBeatmap = new Beatmap + { + HitObjects = Enumerable.Range(1, 40).Select(i => new TestHitObject(HitResult.Perfect, HitResult.Miss)).ToList(), + }; + scoreProcessor.ApplyBeatmap(testBeatmap); + + var results = new List(); + JudgementResult judgementResult; + + for (int i = 0; i < 25; ++i) + { + judgementResult = new JudgementResult(testBeatmap.HitObjects[i], new TestJudgement(HitResult.Perfect, HitResult.Miss)) + { + Type = HitResult.Perfect + }; + results.Add(judgementResult); + scoreProcessor.ApplyResult(judgementResult); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(i + 1)); + } + + judgementResult = new JudgementResult(testBeatmap.HitObjects[25], new TestJudgement(HitResult.Perfect, HitResult.Miss)) + { + Type = HitResult.Miss + }; + results.Add(judgementResult); + scoreProcessor.ApplyResult(judgementResult); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); + + for (int i = 26; i < 40; ++i) + { + judgementResult = new JudgementResult(testBeatmap.HitObjects[i], new TestJudgement(HitResult.Perfect, HitResult.Miss)) + { + Type = HitResult.Perfect + }; + results.Add(judgementResult); + scoreProcessor.ApplyResult(judgementResult); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(i - 25)); + } + + Assert.That(scoreProcessor.MaximumStatistics[HitResult.Perfect], Is.EqualTo(40)); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(14)); + Assert.That(scoreProcessor.HighestCombo.Value, Is.EqualTo(25)); + + // random shuffle is VERY extreme and overkill. + // it might not work correctly for any other `ScoreProcessor` property, and the intermediate results likely make no sense. + // the goal is only to demonstrate idempotency to zero when reverting all results. + var random = new Random(20250519); + var toRevert = shuffleResults ? results.OrderBy(_ => random.Next()).ToList() : Enumerable.Reverse(results); + + foreach (var result in toRevert) + scoreProcessor.RevertResult(result); + + Assert.That(scoreProcessor.Combo.Value, Is.Zero); + Assert.That(scoreProcessor.HighestCombo.Value, Is.Zero); + } + private class TestJudgement : Judgement { public override HitResult MaxResult { get; } diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index fed26d8acb..2f31911fac 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -16,6 +16,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Beatmaps.Drawables.Cards.Buttons; +using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -63,7 +64,11 @@ namespace osu.Game.Tests.Visual.Beatmaps withStatistics.NominationStatus = new BeatmapSetNominationStatus { Current = 1, - Required = 2 + RequiredMeta = + { + MainRuleset = 2, + NonMainRuleset = 1, + } }; var undownloadable = getUndownloadableBeatmapSet(); @@ -78,7 +83,11 @@ namespace osu.Game.Tests.Visual.Beatmaps someDifficulties.NominationStatus = new BeatmapSetNominationStatus { Current = 2, - Required = 2 + RequiredMeta = + { + MainRuleset = 2, + NonMainRuleset = 1, + } }; var manyDifficulties = getManyDifficultiesBeatmapSet(100); @@ -220,6 +229,9 @@ namespace osu.Game.Tests.Visual.Beatmaps } private Drawable createContent(OverlayColourScheme colourScheme, Func creationFunc) + => createContent(colourScheme, testCases.Select(creationFunc).ToArray()); + + private Drawable createContent(OverlayColourScheme colourScheme, Drawable[] cards) { var colourProvider = new OverlayColourProvider(colourScheme); @@ -247,7 +259,7 @@ namespace osu.Game.Tests.Visual.Beatmaps Direction = FillDirection.Full, Padding = new MarginPadding(10), Spacing = new Vector2(10), - ChildrenEnumerable = testCases.Select(creationFunc) + ChildrenEnumerable = cards } } } @@ -320,5 +332,54 @@ namespace osu.Game.Tests.Visual.Beatmaps BeatmapCardNormal firstCard() => this.ChildrenOfType().First(); } + + [Test] + public void TestNominations() + { + AddStep("create cards", () => + { + var singleRuleset = CreateAPIBeatmapSet(Ruleset.Value); + singleRuleset.HypeStatus = new BeatmapSetHypeStatus(); + singleRuleset.NominationStatus = new BeatmapSetNominationStatus + { + Current = 4, + RequiredMeta = + { + MainRuleset = 5, + NonMainRuleset = 1, + } + }; + + var multipleRulesets = getManyDifficultiesBeatmapSet(3); + multipleRulesets.HypeStatus = new BeatmapSetHypeStatus(); + multipleRulesets.NominationStatus = new BeatmapSetNominationStatus + { + Current = 4, + RequiredMeta = + { + MainRuleset = 5, + NonMainRuleset = 1, + } + }; + + Child = createContent(OverlayColourScheme.Blue, new Drawable[] + { + new BeatmapCardNormal(singleRuleset), + new BeatmapCardNormal(multipleRulesets), + }); + }); + + // first card: only has main ruleset, required nominations = main_ruleset = 5 + AddAssert("first card has single ruleset", () => firstCard().BeatmapSet.Beatmaps.GroupBy(b => b.Ruleset).Count(), () => Is.EqualTo(1)); + AddAssert("first card nominations = 4/5", () => firstCard().ChildrenOfType().Single().TooltipText.ToString(), () => Is.EqualTo("Nominations: 4/5")); + + // second card: has non-main rulesets, required nominations = main_ruleset + non_main_ruleset * (count of non-main rulesets) = 5 + 1 * 2 = 7 + AddAssert("second card has three rulesets", () => secondCard().BeatmapSet.Beatmaps.GroupBy(b => b.Ruleset).Count(), () => Is.EqualTo(3)); + AddAssert("second card nominations = 4/7", () => secondCard().ChildrenOfType().Single().TooltipText.ToString(), () => Is.EqualTo("Nominations: 4/7")); + + // order is reversed due to the cards being inside a reverse child-id fill flow. + BeatmapCardNormal firstCard() => this.ChildrenOfType().ElementAt(1); + BeatmapCardNormal secondCard() => this.ChildrenOfType().ElementAt(0); + } } } diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs index 82e02a9b6f..1651adc08f 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs @@ -20,6 +20,7 @@ namespace osu.Game.Tests.Visual.Beatmaps public partial class TestSceneBeatmapSetOnlineStatusPill : ThemeComparisonTestScene { private bool showUnknownStatus; + private bool animated = true; protected override Drawable CreateContent() => new FillFlowContainer { @@ -37,10 +38,11 @@ namespace osu.Game.Tests.Visual.Beatmaps new BeatmapSetOnlineStatusPill { ShowUnknownStatus = showUnknownStatus, + Animated = animated, Anchor = Anchor.Centre, Origin = Anchor.Centre, Status = status - } + }, } }) }; @@ -64,6 +66,12 @@ namespace osu.Game.Tests.Visual.Beatmaps CreateThemedContent(OverlayColourScheme.Red); }); + AddStep("toggle animate", () => + { + animated = !animated; + CreateThemedContent(OverlayColourScheme.Red); + }); + AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both)); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboardProvider.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboardProvider.cs index 964f53c973..5ba6b5432c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboardProvider.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboardProvider.cs @@ -37,6 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay TotalScore = 10_000 * (100 - i), Position = i, }).ToArray(), + 1337, null ); }); @@ -83,6 +84,7 @@ namespace osu.Game.Tests.Visual.Gameplay TotalScore = 600_000 + 10_000 * (40 - i), Position = i, }).ToArray(), + 1337, null ); }); @@ -129,6 +131,7 @@ namespace osu.Game.Tests.Visual.Gameplay TotalScore = 500_000 + 10_000 * (50 - i), Position = i }).ToArray(), + 1337, new ScoreInfo { TotalScore = 200_000 } ); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs index 42f34539de..228ae4eb1a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPositionDisplay.cs @@ -86,5 +86,39 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change local user", () => ((DummyAPIAccess)API).LocalUser.Value = new GuestUser()); AddUntilStep("display hidden", () => display.Alpha, () => Is.EqualTo(0)); } + + [Test] + public void TestTwoPlayers() + { + AddStep("create content", () => + { + Children = new Drawable[] + { + new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = + [ + (typeof(IGameplayLeaderboardProvider), leaderboardProvider = new TestSceneGameplayLeaderboard.TestGameplayLeaderboardProvider()), + (typeof(GameplayState), gameplayState = TestGameplayState.Create(new OsuRuleset())) + ], + Child = display = new MultiplayerPositionDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; + + score = leaderboardProvider.CreateLeaderboardScore(new BindableLong(), API.LocalUser.Value, true); + score.Position.BindTo(position); + + var r = leaderboardProvider.CreateRandomScore(new APIUser()); + r.Position.Value = 1; + }); + + AddStep("first place", () => position.Value = 1); + AddStep("second place", () => position.Value = 2); + } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenFooterNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenFooterNavigation.cs index bc943a876b..3b1334283e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenFooterNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenFooterNavigation.cs @@ -1,10 +1,13 @@ // Copyright (c) ppy Pty Ltd . 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 NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Overlays; using osu.Game.Screens; using osu.Game.Screens.Footer; @@ -14,6 +17,19 @@ namespace osu.Game.Tests.Visual.Navigation { private ScreenFooter screenFooter => this.ChildrenOfType().Single(); + [Test] + public void TestFooterButtonsOnScreenTransitions() + { + PushAndConfirm(() => new TestScreenOne()); + AddUntilStep("button one shown", () => screenFooter.ChildrenOfType().First().Text.ToString(), () => Is.EqualTo("Button One")); + + PushAndConfirm(() => new TestScreenTwo()); + AddUntilStep("button two shown", () => screenFooter.ChildrenOfType().First().Text.ToString(), () => Is.EqualTo("Button Two")); + + AddStep("exit screen", () => Game.ScreenStack.Exit()); + AddUntilStep("button one shown", () => screenFooter.ChildrenOfType().First().Text.ToString(), () => Is.EqualTo("Button One")); + } + [Test] public void TestFooterHidesOldBackButton() { @@ -38,6 +54,32 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("old back button shown", () => Game.BackButton.State.Value, () => Is.EqualTo(Visibility.Visible)); } + private partial class TestScreenOne : OsuScreen + { + public override bool ShowFooter => true; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + public override IReadOnlyList CreateFooterButtons() => new[] + { + new ScreenFooterButton { Text = "Button One" }, + }; + } + + private partial class TestScreenTwo : OsuScreen + { + public override bool ShowFooter => true; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + public override IReadOnlyList CreateFooterButtons() => new[] + { + new ScreenFooterButton { Text = "Button Two" }, + }; + } + private partial class TestScreen : OsuScreen { public override bool ShowFooter { get; } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs index c9ef508a84..1ea5e13c49 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSoloResultsScreen.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show results", () => LoadScreen(new SoloResultsScreen(localScore))); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); - AddAssert("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); + AddUntilStep("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); } [Test] @@ -125,7 +125,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show results", () => LoadScreen(new SoloResultsScreen(localScore))); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); - AddAssert("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); + AddUntilStep("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); } [Test] @@ -164,7 +164,7 @@ namespace osu.Game.Tests.Visual.Ranking LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); - AddAssert("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); + AddUntilStep("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); } [Test] @@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual.Ranking LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); - AddAssert("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); + AddUntilStep("local score is #16", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(16)); AddAssert("previous user best not shown", () => this.ChildrenOfType().All(p => p.Score.OnlineID != 123456)); } @@ -254,7 +254,7 @@ namespace osu.Game.Tests.Visual.Ranking LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); - AddAssert("local score is #31", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(31)); + AddUntilStep("local score is #31", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(31)); } [Test] @@ -306,7 +306,7 @@ namespace osu.Game.Tests.Visual.Ranking }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); AddAssert("local score has no position", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.Null); - AddAssert("previous user best shown at same position", () => this.ChildrenOfType().Any(p => p.Score.OnlineID == 123456 && p.ScorePosition.Value == 133_337)); + AddUntilStep("previous user best shown at same position", () => this.ChildrenOfType().Any(p => p.Score.OnlineID == 123456 && p.ScorePosition.Value == 133_337)); } [Test] @@ -411,7 +411,7 @@ namespace osu.Game.Tests.Visual.Ranking LoadScreen(new SoloResultsScreen(localScore)); }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); - AddAssert("local score is #36", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(36)); + AddUntilStep("local score is #36", () => this.ChildrenOfType().Single().GetPanelForScore(localScore).ScorePosition.Value, () => Is.EqualTo(36)); AddAssert("previous user best not shown", () => this.ChildrenOfType().All(p => p.Score.OnlineID != 123456)); } @@ -463,7 +463,7 @@ namespace osu.Game.Tests.Visual.Ranking }); AddUntilStep("wait for loaded", () => ((Drawable)Stack.CurrentScreen).IsLoaded); AddAssert("only one score with ID 12345", () => this.ChildrenOfType().Count(s => s.Score.OnlineID == 12345), () => Is.EqualTo(1)); - AddAssert("user best position preserved", () => this.ChildrenOfType().Any(p => p.ScorePosition.Value == 133_337)); + AddUntilStep("user best position preserved", () => this.ChildrenOfType().Any(p => p.ScorePosition.Value == 133_337)); } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs index 379bd838cd..a9829fbf0c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.SongSelect } } - public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, CancellationToken cancellationToken = default) + public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, CancellationToken cancellationToken = default, int debounceDelay = 0) { if (blockCalculation) { @@ -149,7 +149,7 @@ namespace osu.Game.Tests.Visual.SongSelect await calculationBlocker.Task.ConfigureAwait(false); } - return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false); + return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken, 0).ConfigureAwait(false); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs index 4ea56d3150..0f991abcfc 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/BeatmapCarouselTestScene.cs @@ -47,6 +47,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + public Func, BeatmapInfo>? BeatmapRecommendationFunction { get; set; } + private OsuTextFlowContainer stats = null!; private int beatmapCount; @@ -69,6 +71,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("create components", () => { + BeatmapRecommendationFunction = null; NewItemsPresentedInvocationCount = 0; Box topBox; @@ -105,6 +108,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Carousel = new TestBeatmapCarousel { NewItemsPresented = () => NewItemsPresentedInvocationCount++, + ChooseRecommendedBeatmap = beatmaps => BeatmapRecommendationFunction?.Invoke(beatmaps) ?? beatmaps.First(), BleedTop = 50, BleedBottom = 50, Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index 4ca6c5a549..85d08949fb 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -189,10 +189,24 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private void updateFooter(IScreen? _, IScreen? newScreen) { - if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter) + if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter) { Footer.Show(); - Footer.SetButtons(osuScreen.CreateFooterButtons()); + + if (osuScreen.IsLoaded) + updateFooterButtons(); + else + osuScreen.OnLoadComplete += _ => updateFooterButtons(); + + void updateFooterButtons() + { + var buttons = osuScreen.CreateFooterButtons(); + + osuScreen.LoadComponentsAgainstScreenDependencies(buttons); + + Footer.SetButtons(buttons); + Footer.Show(); + } } else { diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs index 21030e0b88..ae3d95451e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarousel.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Utils; @@ -62,6 +64,26 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("enable masking", () => Scroll.Masking = true); } + [Test] + [Explicit] + public void TestRandomStatus() + { + SortBy(SortMode.Title); + AddStep("add beatmaps", () => + { + for (int i = 0; i < 50; i++) + { + var set = TestResources.CreateTestBeatmapSetInfo(); + set.Status = Enum.GetValues().MinBy(_ => RNG.Next()); + + if (i % 2 == 0) + set.Status = BeatmapOnlineStatus.None; + + BeatmapSets.Add(set); + } + }); + } + [Test] [Explicit] public void TestPerformanceWithManyBeatmaps() diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs index 805d5d3213..146d042ca0 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselFiltering.cs @@ -290,5 +290,26 @@ namespace osu.Game.Tests.Visual.SongSelectV2 CheckDisplayedBeatmapsCount(local_set_count); } + + [Test] + public void TestFirstDifficultyFiltered() + { + AddBeatmaps(2, 3); + WaitForDrawablePanels(); + + SelectNextGroup(); + WaitForSelection(0, 0); + + CheckDisplayedBeatmapsCount(6); + + ApplyToFilter("filter first away", c => c.UserStarDifficulty.Min = 3); + WaitForFiltering(); + + CheckDisplayedBeatmapsCount(4); + + SelectNextGroup(); + SelectPrevGroup(); + WaitForSelection(0, 1); + } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs index 3ca8773adb..7214cb1e16 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselNoGrouping.cs @@ -261,6 +261,40 @@ namespace osu.Game.Tests.Visual.SongSelectV2 WaitForSelection(1, 1); } + [Test] + public void TestRecommendedSelection() + { + AddBeatmaps(5, 3); + WaitForDrawablePanels(); + + AddStep("set recommendation algorithm", () => BeatmapRecommendationFunction = beatmaps => beatmaps.Last()); + + SelectPrevGroup(); + + // check recommended was selected + SelectNextGroup(); + WaitForSelection(0, 2); + + // change away from recommended + SelectPrevPanel(); + Select(); + WaitForSelection(0, 1); + + // next set, check recommended + SelectNextGroup(); + WaitForSelection(1, 2); + + // next set, check recommended + SelectNextGroup(); + WaitForSelection(2, 2); + + // go back to first set and ensure user selection was retained + // todo: we don't do that yet. not sure if we will continue to have this. + // SelectPrevGroup(); + // SelectPrevGroup(); + // WaitForSelection(0, 1); + } + private void checkSelectionIterating(bool isIterating) { object? selection = null; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs index 61d23c4513..992651d73c 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs @@ -312,7 +312,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Username = @"waaiiru", CountryCode = CountryCode.ES, }, - }); + Date = DateTimeOffset.Now, + }, 1234567); } private void showPersonalBest() @@ -332,8 +333,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Id = 6602580, Username = @"waaiiru", CountryCode = CountryCode.ES, - } - }); + }, + Date = DateTimeOffset.Now, + }, 1234567); } private void setScope(BeatmapLeaderboardScope scope) @@ -364,7 +366,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2 private partial class TestBeatmapLeaderboardWedge : BeatmapLeaderboardWedge { public new void SetState(LeaderboardState state) => base.SetState(state); - public new void SetScores(IEnumerable scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore); + public new void SetScores(IEnumerable scores, ScoreInfo? userScore = null, int? totalCount = null) => base.SetScores(scores, userScore, totalCount); } } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs index 3cdb513b38..f18250402e 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Models; using osu.Game.Online.API; @@ -24,30 +25,37 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { base.LoadComplete(); - ((DummyAPIAccess)API).HandleRequest = request => - { - switch (request) - { - case GetBeatmapSetRequest set: - if (set.ID == currentOnlineSet?.OnlineID) - { - set.TriggerSuccess(currentOnlineSet); - return true; - } - - return false; - - default: - return false; - } - }; - Child = wedge = new BeatmapMetadataWedge { State = { Value = Visibility.Visible }, }; } + [SetUpSteps] + public override void SetUpSteps() + { + AddStep("register request handling", () => + { + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest set: + if (set.ID == currentOnlineSet?.OnlineID) + { + set.TriggerSuccess(currentOnlineSet); + return true; + } + + return false; + + default: + return false; + } + }; + }); + } + [Test] public void TestShowHide() { @@ -205,6 +213,74 @@ namespace osu.Game.Tests.Visual.SongSelectV2 }); } + [Test] + public void TestLoading() + { + AddStep("override request handling", () => + { + currentOnlineSet = null; + + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest set: + Scheduler.AddDelayed(() => set.TriggerSuccess(currentOnlineSet!), 500); + return true; + + default: + return false; + } + }; + }); + + AddStep("set beatmap", () => + { + var (working, onlineSet) = createTestBeatmap(); + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddWaitStep("wait", 5); + + AddStep("set beatmap", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.RelatedTags![0].Name = "other/tag"; + onlineSet.RelatedTags[1].Name = "another/tag"; + onlineSet.RelatedTags[2].Name = "some/tag"; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddWaitStep("wait", 5); + + AddStep("no user tags", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Beatmaps.Single().TopTags = null; + onlineSet.RelatedTags = null; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddWaitStep("wait", 5); + + AddStep("no user tags", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Beatmaps.Single().TopTags = null; + onlineSet.RelatedTags = null; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddWaitStep("wait", 5); + } + private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap() { var working = CreateWorkingBeatmap(Ruleset.Value); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 5d96ebaa85..fcb74e539b 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Online.API; -using osu.Game.Online.Leaderboards; using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; @@ -66,7 +65,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("set local scope", () => { var current = LeaderboardManager.CurrentCriteria!; - LeaderboardManager.FetchWithCriteria(new LeaderboardCriteria(current.Beatmap, current.Ruleset, BeatmapLeaderboardScope.Local, null)); + LeaderboardManager.FetchWithCriteria(current with + { + Scope = BeatmapLeaderboardScope.Local, + }); }); AddUntilStep("wait for score panel", () => SongSelect.ChildrenOfType().Any()); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 4e2e8ea332..c0c80e0bc3 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -246,6 +246,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2 checkMatchedBeatmaps(0); } + [Test] + public void TestHideBeatmap() + { + LoadSongSelect(); + ImportBeatmapForRuleset(0); + + checkMatchedBeatmaps(3); + + // song select should automatically select the beatmap for us but this is not implemented yet. + // todo: remove when that's the case. + AddAssert("no beatmap selected", () => Beatmap.IsDefault); + AddStep("select beatmap", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(Beatmaps.GetAllUsableBeatmapSets().Single().Beatmaps.First())); + + AddStep("hide", () => Beatmaps.Hide(Beatmap.Value.BeatmapInfo)); + + checkMatchedBeatmaps(2); + + AddStep("restore", () => Beatmaps.Restore(Beatmap.Value.BeatmapInfo)); + + checkMatchedBeatmaps(3); + } + private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType().FirstOrDefault(); private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected)); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs index cdb6900f06..5bb590a247 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModEffectPreviewPanel.cs @@ -125,7 +125,7 @@ namespace osu.Game.Tests.Visual.UserInterface public StarDifficulty? Difficulty { get; set; } public override Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, int computationDelay = 0) => Task.FromResult(Difficulty); } } diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index fc4175415c..ef7b3bf8cc 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -98,12 +98,17 @@ namespace osu.Game.Beatmaps /// /// The to get the difficulty of. /// An optional which stops updating the star difficulty for the given . + /// A delay in milliseconds before performing the /// A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state (but not during updates to ruleset and mods if a stale value is already propagated). - public IBindable GetBindableDifficulty(IBeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) + public IBindable GetBindableDifficulty(IBeatmapInfo beatmapInfo, CancellationToken cancellationToken = default, int computationDelay = 0) { - var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); + var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken) + { + // Start with an approximate known value instead of zero. + Value = new StarDifficulty(beatmapInfo.StarRating, 0) + }; - updateBindable(bindable, currentRuleset.Value, currentMods.Value, cancellationToken); + updateBindable(bindable, currentRuleset.Value, currentMods.Value, cancellationToken, computationDelay); lock (bindableUpdateLock) trackedBindables.Add(bindable); @@ -118,13 +123,14 @@ namespace osu.Game.Beatmaps /// The to get the difficulty with. /// The s to get the difficulty with. /// An optional which stops computing the star difficulty. + /// In the case a cached lookup was not possible, a value in milliseconds of to wait until performing potentially intensive lookup. /// /// The requested , if non-. /// A return value indicates that the difficulty process failed or was interrupted early, /// and as such there is no usable star difficulty value to be returned. /// - public virtual Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, - IEnumerable? mods = null, CancellationToken cancellationToken = default) + public virtual Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, + CancellationToken cancellationToken = default, int computationDelay = 0) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; @@ -139,7 +145,7 @@ namespace osu.Game.Beatmaps return Task.FromResult(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0)); } - return GetAsync(new DifficultyCacheLookup(localBeatmapInfo, localRulesetInfo, mods), cancellationToken); + return GetAsync(new DifficultyCacheLookup(localBeatmapInfo, localRulesetInfo, mods), cancellationToken, computationDelay); } protected override Task ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken cancellationToken = default) @@ -206,11 +212,12 @@ namespace osu.Game.Beatmaps /// The to update with. /// The s to update with. /// A token that may be used to cancel this update. - private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable? mods, CancellationToken cancellationToken = default) + /// In the case a cached lookup was not possible, a value in milliseconds of to wait until performing potentially intensive lookup. + private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable? mods, CancellationToken cancellationToken = default, int computationDelay = 0) { // GetDifficultyAsync will fall back to existing data from IBeatmapInfo if not locally available // (contrary to GetAsync) - GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken) + GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken, computationDelay) .ContinueWith(task => { // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events. diff --git a/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs b/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs index 8cf43ab320..267e46b0d4 100644 --- a/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs +++ b/osu.Game/Beatmaps/BeatmapSetNominationStatus.cs @@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps /// /// The number of nominations required so that the map is eligible for qualification. /// - [JsonProperty(@"required")] - public int Required { get; set; } + [JsonProperty(@"required_meta")] + public BeatmapSetNominationRequiredMeta RequiredMeta { get; set; } = new BeatmapSetNominationRequiredMeta(); } } diff --git a/osu.Game/Beatmaps/BeatmapSetNominationStatusRequiredMeta.cs b/osu.Game/Beatmaps/BeatmapSetNominationStatusRequiredMeta.cs new file mode 100644 index 0000000000..44d31d7b2c --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetNominationStatusRequiredMeta.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + /// + /// Contains information about the number of nominations required for a beatmap set. + /// + public class BeatmapSetNominationRequiredMeta + { + /// + /// The number of nominations required for difficulties of the main ruleset. + /// + [JsonProperty(@"main_ruleset")] + public int MainRuleset { get; set; } + + /// + /// The number of nominations required for difficulties of each non-main ruleset. + /// + [JsonProperty(@"non_main_ruleset")] + public int NonMainRuleset { get; set; } + } +} diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs index c6a3c7db3c..b10ea4fa75 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetOnlineStatusPill.cs @@ -24,6 +24,11 @@ namespace osu.Game.Beatmaps.Drawables /// public bool ShowUnknownStatus { get; init; } + /// + /// Whether changing status performs transition transforms. + /// + public bool Animated { get; init; } = true; + public BeatmapOnlineStatus Status { get => status; @@ -98,9 +103,11 @@ namespace osu.Game.Beatmaps.Drawables private void updateState() { + double duration = Animated ? animation_duration : 0; + if (Status == BeatmapOnlineStatus.None && !ShowUnknownStatus) { - this.FadeOut(animation_duration, Easing.OutQuint); + this.FadeOut(duration, Easing.OutQuint); return; } @@ -109,15 +116,16 @@ namespace osu.Game.Beatmaps.Drawables // after we have a valid size. if (Height > 0) { - AutoSizeDuration = (float)animation_duration; + AutoSizeDuration = (float)duration; AutoSizeEasing = Easing.OutQuint; } - this.FadeIn(animation_duration, Easing.OutQuint); + this.FadeIn(duration, Easing.OutQuint); // Handle the case where transition from hidden to non-hidden may cause // a fade from a colour that doesn't make sense (due to not being able to see the previous colour). - double duration = Alpha > 0 ? animation_duration : 0; + if (Alpha == 0) + duration = 0; Color4 statusTextColour; diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs index 083f1a353b..01f6fde256 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/NominationsStatistic.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; namespace osu.Game.Beatmaps.Drawables.Cards.Statistics @@ -12,16 +14,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics /// public partial class NominationsStatistic : BeatmapCardStatistic { - private NominationsStatistic(BeatmapSetNominationStatus nominationStatus) + private NominationsStatistic(int current, int required) { Icon = FontAwesome.Solid.ThumbsUp; - Text = nominationStatus.Current.ToLocalisableString(); - TooltipText = BeatmapsStrings.NominationsRequiredText(nominationStatus.Current.ToLocalisableString(), nominationStatus.Required.ToLocalisableString()); + Text = current.ToLocalisableString(); + TooltipText = BeatmapsStrings.NominationsRequiredText(current.ToLocalisableString(), required.ToLocalisableString()); } - public static NominationsStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetOnlineInfo) + public static NominationsStatistic? CreateFor(APIBeatmapSet beatmapSet) + { // web does not show nominations unless hypes are also present. // see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443 - => beatmapSetOnlineInfo.HypeStatus == null || beatmapSetOnlineInfo.NominationStatus == null ? null : new NominationsStatistic(beatmapSetOnlineInfo.NominationStatus); + if (beatmapSet.HypeStatus == null || beatmapSet.NominationStatus == null) + return null; + + int current = beatmapSet.NominationStatus.Current; + int requiredMainRuleset = beatmapSet.NominationStatus.RequiredMeta.MainRuleset; + int requiredNonMainRuleset = beatmapSet.NominationStatus.RequiredMeta.NonMainRuleset; + + int rulesets = beatmapSet.Beatmaps.GroupBy(b => b.Ruleset).Count(); + + return new NominationsStatistic(current, requiredMainRuleset + requiredNonMainRuleset * (rulesets - 1)); + } } } diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 2e7f894d12..92db97475a 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Rulesets; @@ -20,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables { - public partial class DifficultyIcon : CompositeDrawable, IHasCustomTooltip, IHasCurrentValue + public partial class DifficultyIcon : CompositeDrawable, IHasCustomTooltip { /// /// Size of this difficulty icon. @@ -46,8 +45,12 @@ namespace osu.Game.Beatmaps.Drawables private readonly Container iconContainer; + [Resolved] + private OsuColour colours { get; set; } = null!; + private readonly BindableWithCurrent difficulty = new BindableWithCurrent(); + // TODO: remove this after old song select is gone. public virtual Bindable Current { get => difficulty.Current; @@ -64,28 +67,19 @@ namespace osu.Game.Beatmaps.Drawables /// An array of mods to account for in the calculations /// An optional ruleset to be used for the icon display, in place of the beatmap's ruleset. public DifficultyIcon(IBeatmapInfo beatmap, IRulesetInfo? ruleset = null, Mod[]? mods = null) - : this(ruleset ?? beatmap.Ruleset) { this.beatmap = beatmap; this.mods = mods; + this.ruleset = ruleset ?? beatmap.Ruleset; Current.Value = new StarDifficulty(beatmap.StarRating, 0); - } - - /// - /// Creates a new without an associated beatmap. - /// - /// The ruleset to be used for the icon display. - public DifficultyIcon(IRulesetInfo ruleset) - { - this.ruleset = ruleset; AutoSizeAxes = Axes.Both; InternalChild = iconContainer = new Container { Size = new Vector2(20f) }; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { iconContainer.Children = new Drawable[] { @@ -115,8 +109,18 @@ namespace osu.Game.Beatmaps.Drawables Icon = getRulesetIcon() }, }; + } - Current.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars), true); + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(difficulty => + { + background.FadeColour(colours.ForStarDifficulty(difficulty.NewValue.Stars), 200); + }, true); + + background.FinishTransforms(); } private Drawable getRulesetIcon() diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs index 93d1f5d5c5..c9f2f8a4b1 100644 --- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -23,8 +24,6 @@ namespace osu.Game.Beatmaps.Drawables /// public partial class StarRatingDisplay : CompositeDrawable, IHasCurrentValue { - public const double TRANSFORM_DURATION = 750; - private readonly bool animated; private readonly Box background; private readonly SpriteIcon starIcon; @@ -147,7 +146,8 @@ namespace osu.Game.Beatmaps.Drawables Current.BindValueChanged(c => { if (animated) - this.TransformBindableTo(displayedStars, c.NewValue.Stars, TRANSFORM_DURATION, Easing.OutQuint); + // Animation roughly matches `StarCounter`'s implementation. + this.TransformBindableTo(displayedStars, c.NewValue.Stars, 100 + 80 * Math.Abs(c.NewValue.Stars - c.OldValue.Stars), Easing.OutQuint); else displayedStars.Value = c.NewValue.Stars; }); diff --git a/osu.Game/Configuration/ReleaseStream.cs b/osu.Game/Configuration/ReleaseStream.cs index ed0bee1dd8..d4f382099c 100644 --- a/osu.Game/Configuration/ReleaseStream.cs +++ b/osu.Game/Configuration/ReleaseStream.cs @@ -1,13 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; + namespace osu.Game.Configuration { public enum ReleaseStream { Lazer, - //Stable40, - //Beta40, - //Stable + + [Description("Tachyon (Unstable)")] + Tachyon } } diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs index e98475efae..a91c608279 100644 --- a/osu.Game/Database/MemoryCachingComponent.cs +++ b/osu.Game/Database/MemoryCachingComponent.cs @@ -35,8 +35,9 @@ namespace osu.Game.Database /// Retrieve the cached value for the given lookup. /// /// The lookup to retrieve. - /// An optional to cancel the operation. - protected async Task GetAsync(TLookup lookup, CancellationToken token = default) + /// An optional to cancel the operation. + /// In the case a cached lookup was not possible, a value in milliseconds of to wait until performing potentially intensive lookup. + protected async Task GetAsync(TLookup lookup, CancellationToken cancellationToken = default, int computationDelay = 0) { if (CheckExists(lookup, out TValue? existing)) { @@ -44,7 +45,10 @@ namespace osu.Game.Database return existing; } - var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false); + if (computationDelay > 0) + await Task.Delay(computationDelay, cancellationToken).ConfigureAwait(false); + + var computed = await ComputeValueAsync(lookup, cancellationToken).ConfigureAwait(false); statistics.Value.MissCount++; diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs index 83a3af574c..0f4dd0805e 100644 --- a/osu.Game/Localisation/GeneralSettingsStrings.cs +++ b/osu.Game/Localisation/GeneralSettingsStrings.cs @@ -79,6 +79,18 @@ namespace osu.Game.Localisation /// public static LocalisableString LearnMoreAboutLazerTooltip => new TranslatableString(getKey(@"check_out_the_feature_comparison"), @"Check out the feature comparison and FAQ"); + /// + /// "Are you sure you want to run a potentially unstable version of the game?" + /// + public static LocalisableString ChangeReleaseStreamConfirmation => new TranslatableString(getKey(@"change_release stream_confirmation"), + @"Are you sure you want to run a potentially unstable version of the game?"); + + /// + /// "If you run into issues starting the game, you can usually run the installer from the official site to recover." + /// + public static LocalisableString ChangeReleaseStreamConfirmationInfo => new TranslatableString(getKey(@"change_release stream_confirmation_info"), + @"If you run into issues starting the game, you can usually run the installer from the official site to recover."); + /// /// "You are running the latest release ({0})" /// diff --git a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs index 4ef39be5e5..ae73e377c4 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs +++ b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs @@ -10,6 +10,9 @@ namespace osu.Game.Online.API.Requests.Responses { public class APIScoresCollection { + [JsonProperty(@"score_count")] + public int ScoresCount; + [JsonProperty(@"scores")] public List Scores; diff --git a/osu.Game/Online/Leaderboards/LeaderboardManager.cs b/osu.Game/Online/Leaderboards/LeaderboardManager.cs index 4aca3b1a4a..d5d1672e1b 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardManager.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardManager.cs @@ -133,6 +133,7 @@ namespace osu.Game.Online.Leaderboards return s; }) .ToArray(), + response.ScoresCount, response.UserScore?.CreateScoreInfo(rulesets, newCriteria.Beatmap) ); inFlightOnlineRequest = null; @@ -181,7 +182,8 @@ namespace osu.Game.Online.Leaderboards newScores = newScores.Detach().OrderByTotalScore(); - scores.Value = LeaderboardScores.Success(newScores.ToArray(), null); + var newScoresArray = newScores.ToArray(); + scores.Value = LeaderboardScores.Success(newScoresArray, newScoresArray.Length, null); } } @@ -195,6 +197,7 @@ namespace osu.Game.Online.Leaderboards public record LeaderboardScores { public ICollection TopScores { get; } + public int TotalScores { get; } public ScoreInfo? UserScore { get; } public LeaderboardFailState? FailState { get; } @@ -210,15 +213,16 @@ namespace osu.Game.Online.Leaderboards } } - private LeaderboardScores(ICollection topScores, ScoreInfo? userScore, LeaderboardFailState? failState) + private LeaderboardScores(ICollection topScores, int totalScores, ScoreInfo? userScore, LeaderboardFailState? failState) { TopScores = topScores; + TotalScores = totalScores; UserScore = userScore; FailState = failState; } - public static LeaderboardScores Success(ICollection topScores, ScoreInfo? userScore) => new LeaderboardScores(topScores, userScore, null); - public static LeaderboardScores Failure(LeaderboardFailState failState) => new LeaderboardScores([], null, failState); + public static LeaderboardScores Success(ICollection topScores, int totalScores, ScoreInfo? userScore) => new LeaderboardScores(topScores, totalScores, userScore, null); + public static LeaderboardScores Failure(LeaderboardFailState failState) => new LeaderboardScores([], 0, null, failState); } public enum LeaderboardFailState diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 76e5cb0404..dd0e03463c 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -95,7 +95,7 @@ namespace osu.Game.Online.Spectator private readonly Queue pendingFrameBundles = new Queue(); - private readonly Queue pendingFrames = new Queue(); + private readonly List pendingFrames = new List(); private double lastPurgeTime; @@ -244,7 +244,18 @@ namespace osu.Game.Online.Spectator if (frame is IConvertibleReplayFrame convertible) { Debug.Assert(currentBeatmap != null); - pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap)); + + var convertedFrame = convertible.ToLegacy(currentBeatmap); + + // only keep the last recorded frame for a given timestamp. + // this reduces redundancy of frames in the resulting replay. + // + // this is also done at `ReplayRecorded`, but needs to be done here as well + // due to the flow being handled differently. + if (pendingFrames.LastOrDefault()?.Time == convertedFrame.Time) + pendingFrames[^1] = convertedFrame; + else + pendingFrames.Add(convertedFrame); } if (pendingFrames.Count > max_pending_frames) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 473a3a6327..6d12a4a308 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1740,7 +1740,12 @@ namespace osu.Game BackButton.Hide(); ScreenFooter.Show(); - newOsuScreen.OnLoadComplete += _ => + if (newOsuScreen.IsLoaded) + updateFooterButtons(); + else + newOsuScreen.OnLoadComplete += _ => updateFooterButtons(); + + void updateFooterButtons() { var buttons = newScreen.CreateFooterButtons(); @@ -1748,7 +1753,7 @@ namespace osu.Game ScreenFooter.SetButtons(buttons); ScreenFooter.Show(); - }; + } } else { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4cc9ab7936..4da9970ea2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -61,6 +61,7 @@ using osu.Game.Resources; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; +using osu.Game.Screens; using osu.Game.Skinning; using osu.Game.Utils; using RuntimeInfo = osu.Framework.RuntimeInfo; @@ -168,6 +169,12 @@ namespace osu.Game protected Storage Storage { get; set; } + protected EzSkinSettingsManager EzSkinSettingsManager { get; private set; } + + protected EzLocalNoteFactory EzLocalNoteFactory { get; private set; } + + // protected EzNoteFactory EzNoteFactory { get; private set; } + /// /// The language in which the game is currently displayed in. /// @@ -431,26 +438,35 @@ namespace osu.Game // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown"; LocalConfig.LookupKeyBindings = l => KeyBindingStore.GetBindingsStringFor(l); + + // 初始化并注册EzSkinSettingsManager + dependencies.Cache(EzSkinSettingsManager = new EzSkinSettingsManager(Storage)); + // dependencies.CacheAs(EzSkinSettingsManager); + dependencies.Cache(EzLocalNoteFactory = new EzLocalNoteFactory(Storage)); + // EzNoteFactory = new EzNoteFactory(Storage); + // dependencies.CacheAs(EzNoteFactory); + // dependencies.Cache(EzNoteFactory = new EzNoteFactory()); } private void updateLanguage() => CurrentLanguage.Value = LanguageExtensions.GetLanguageFor(frameworkLocale.Value, localisationParameters.Value); private void addFilesWarning() { - var realmStore = new RealmFileStore(realm, Storage); - const string filename = "IMPORTANT READ ME.txt"; - if (!realmStore.Storage.Exists(filename)) + if (!Storage.Exists(filename)) { - using (var stream = realmStore.Storage.CreateFileSafely(filename)) + using (var stream = Storage.CreateFileSafely(filename)) using (var textWriter = new StreamWriter(stream)) { - textWriter.WriteLine(@"This folder contains all your user files (beatmaps, skins, replays etc.)"); - textWriter.WriteLine(@"Please do not touch or delete this folder!!"); + textWriter.WriteLine(@"This folder contains all your user files and configuration."); + textWriter.WriteLine(@"Please DO NOT make manual changes to this folder."); textWriter.WriteLine(); - textWriter.WriteLine(@"If you are really looking to completely delete user data, please delete"); - textWriter.WriteLine(@"the parent folder including all other files and directories"); + textWriter.WriteLine(@"- If you want to back up your game files, please back up THE ENTIRETY OF THIS DIRECTORY."); + textWriter.WriteLine(@"- If you want to delete all of your game files, please delete THE ENTIRETY OF THIS DIRECTORY."); + textWriter.WriteLine(); + textWriter.WriteLine(@"To be very clear, the ""files/"" directory inside this directory stores all the raw pieces of your beatmaps, skins, and replays."); + textWriter.WriteLine(@"Importantly, it is NOT the only directory you need a backup of to avoid losing data. If you copy only the ""files/"" directory, YOU WILL LOSE DATA."); textWriter.WriteLine(); textWriter.WriteLine(@"For more information on how these files are organised,"); textWriter.WriteLine(@"see https://github.com/ppy/osu/wiki/User-file-storage"); diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index eea0b087eb..74b523fdec 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Extensions; using osu.Game.Graphics; @@ -295,7 +294,6 @@ namespace osu.Game.Overlays.BeatmapSet icon = new DifficultyIcon(beatmapInfo, ruleset) { TooltipType = DifficultyIconTooltipType.None, - Current = { Value = new StarDifficulty(beatmapInfo.StarRating, 0) }, Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(size - tile_icon_padding * 2), diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 261103173e..ac6215f3ad 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; @@ -13,6 +14,7 @@ using osu.Framework.Statistics; using osu.Game.Configuration; using osu.Game.Localisation; using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Updater; @@ -27,6 +29,9 @@ namespace osu.Game.Overlays.Settings.Sections.General private SettingsButton checkForUpdatesButton = null!; + private readonly Bindable configReleaseStream = new Bindable(); + private SettingsEnumDropdown releaseStreamDropdown = null!; + [Resolved] private UpdateManager? updateManager { get; set; } @@ -40,21 +45,46 @@ namespace osu.Game.Overlays.Settings.Sections.General private OsuGame? game { get; set; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, IDialogOverlay? dialogOverlay) { - Add(new SettingsEnumDropdown - { - LabelText = GeneralSettingsStrings.ReleaseStream, - Current = config.GetBindable(OsuSetting.ReleaseStream), - }); + config.BindWith(OsuSetting.ReleaseStream, configReleaseStream); if (updateManager?.CanCheckForUpdate == true) { + Add(releaseStreamDropdown = new SettingsEnumDropdown + { + LabelText = GeneralSettingsStrings.ReleaseStream, + Current = { Value = configReleaseStream.Value }, + }); + Add(checkForUpdatesButton = new SettingsButton { Text = GeneralSettingsStrings.CheckUpdate, Action = () => checkForUpdates().FireAndForget() }); + + releaseStreamDropdown.Current.BindValueChanged(stream => + { + if (stream.NewValue == ReleaseStream.Tachyon) + { + dialogOverlay?.Push(new ConfirmDialog(GeneralSettingsStrings.ChangeReleaseStreamConfirmation, + () => + { + configReleaseStream.Value = ReleaseStream.Tachyon; + }, + () => + { + releaseStreamDropdown.Current.Value = ReleaseStream.Lazer; + }) + { + BodyText = GeneralSettingsStrings.ChangeReleaseStreamConfirmationInfo + }); + + return; + } + + configReleaseStream.Value = stream.NewValue; + }); } if (RuntimeInfo.IsDesktop) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 17186b731a..132656147e 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -127,6 +127,7 @@ namespace osu.Game.Overlays.Settings.Sections dropdownItems.Clear(); dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.EZ2_SKIN).ToLive(realm)); + dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.EZ_STYLE_PRO_SKIN).ToLive(realm)); dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.SBI_SKIN).ToLive(realm)); dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.ARGON_SKIN).ToLive(realm)); dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.ARGON_PRO_SKIN).ToLive(realm)); diff --git a/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs b/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs index a5ed0d65bd..000afd2c1d 100644 --- a/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/AnalogClockDisplay.cs @@ -70,8 +70,18 @@ namespace osu.Game.Overlays.Toolbar float rotation = fraction * 360 - 90; + // The case where a hand is completing a rotation. + // Animate and then move back one full rotation so we don't need to track outside of 0..360 if (Math.Abs(hand.Rotation - rotation) > 180) - hand.RotateTo(rotation); + { + float animRotation = rotation; + while (animRotation < hand.Rotation) + animRotation += 180; + + hand.RotateTo(animRotation, duration, Easing.OutElastic) + .Then() + .RotateTo(rotation); + } else hand.RotateTo(rotation, duration, Easing.OutElastic); } diff --git a/osu.Game/Overlays/Toolbar/ClockDisplay.cs b/osu.Game/Overlays/Toolbar/ClockDisplay.cs index c72c92b61b..0b223c6038 100644 --- a/osu.Game/Overlays/Toolbar/ClockDisplay.cs +++ b/osu.Game/Overlays/Toolbar/ClockDisplay.cs @@ -10,6 +10,14 @@ namespace osu.Game.Overlays.Toolbar { private int? lastSecond; + protected override void LoadComplete() + { + base.LoadComplete(); + + UpdateDisplay(DateTimeOffset.Now); + FinishTransforms(true); + } + protected override void Update() { base.Update(); diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 4b98df50d7..ab83ee62b0 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -74,6 +74,11 @@ namespace osu.Game.Rulesets.Judgements /// public int HighestComboAtJudgement { get; internal set; } + /// + /// The highest combo achieved after this occurred. + /// + public int HighestComboAfterJudgement { get; internal set; } + /// /// The health prior to this occurring. /// diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ed9ed1db0a..799556acdf 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -781,7 +781,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Creates the that represents the scoring result for this . /// /// The that provides the scoring information. - protected virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(HitObject, judgement); + protected internal virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(HitObject, judgement); private void ensureEntryHasResult() { diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index cffbb03eeb..e092f91f22 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -215,7 +215,6 @@ namespace osu.Game.Rulesets.Scoring { Ruleset = ruleset; - Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); Accuracy.ValueChanged += _ => updateRank(); Mods.ValueChanged += mods => @@ -251,7 +250,10 @@ namespace osu.Game.Rulesets.Scoring else if (result.Type.BreaksCombo()) Combo.Value = 0; + HighestCombo.Value = Math.Max(HighestCombo.Value, Combo.Value); + result.ComboAfterJudgement = Combo.Value; + result.HighestComboAfterJudgement = HighestCombo.Value; if (result.Judgement.MaxResult.AffectsAccuracy()) { @@ -294,8 +296,11 @@ namespace osu.Game.Rulesets.Scoring if (!TrackHitEvents) throw new InvalidOperationException(@$"Rewind is not supported when {nameof(TrackHitEvents)} is disabled."); - Combo.Value = result.ComboAtJudgement; - HighestCombo.Value = result.HighestComboAtJudgement; + // the reason this is written so funnily rather than just using `ComboAtJudgement` + // is to nullify impact of ordering when reverting concurrent judgement results + // (think mania and multiple judgements within a frame). + Combo.Value -= (result.ComboAfterJudgement - result.ComboAtJudgement); + HighestCombo.Value -= (result.HighestComboAfterJudgement - result.HighestComboAtJudgement); if (result.FailedAtJudgement && !ApplyNewJudgementsWhenFailed) return; diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 97c4ee45af..6b2387eb9b 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -300,6 +300,7 @@ namespace osu.Game.Rulesets.UI if (score == null) { + NewResult -= emitImportantFrame; recordingInputManager.Recorder = null; return; } @@ -311,7 +312,10 @@ namespace osu.Game.Rulesets.UI recorder.ScreenSpaceToGamefield = Playfield.ScreenSpaceToGamefield; + NewResult += emitImportantFrame; recordingInputManager.Recorder = recorder; + + void emitImportantFrame(JudgementResult judgementResult) => recordingInputManager.Recorder?.RecordFrame(true); } public override void SetReplayScore(Score replayScore) diff --git a/osu.Game/Rulesets/UI/IHasRecordingHandler.cs b/osu.Game/Rulesets/UI/IHasRecordingHandler.cs index f73398dd98..f2e153e238 100644 --- a/osu.Game/Rulesets/UI/IHasRecordingHandler.cs +++ b/osu.Game/Rulesets/UI/IHasRecordingHandler.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.UI /// public interface IHasRecordingHandler { - public ReplayRecorder? Recorder { set; } + public ReplayRecorder? Recorder { get; set; } } } diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index d723c31434..c2187b0634 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -51,29 +51,29 @@ namespace osu.Game.Rulesets.UI protected override void Update() { base.Update(); - recordFrame(false); + RecordFrame(false); } protected override bool OnMouseMove(MouseMoveEvent e) { - recordFrame(false); + RecordFrame(false); return base.OnMouseMove(e); } public bool OnPressed(KeyBindingPressEvent e) { pressedActions.Add(e.Action); - recordFrame(true); + RecordFrame(true); return false; } public void OnReleased(KeyBindingReleaseEvent e) { pressedActions.Remove(e.Action); - recordFrame(true); + RecordFrame(true); } - private void recordFrame(bool important) + public override void RecordFrame(bool important) { var last = target.Replay.Frames.LastOrDefault(); @@ -86,8 +86,15 @@ namespace osu.Game.Rulesets.UI if (frame != null) { - target.Replay.Frames.Add(frame); + // only keep the last recorded frame for a given timestamp. + // this reduces redundancy of frames in the resulting replay. + if (last?.Time == frame.Time) + target.Replay.Frames[^1] = frame; + else + target.Replay.Frames.Add(frame); + // the above de-duplication is done at `FrameDataBundle` level in `SpectatorClient`. + // it's not 100% matching because of the possibility of duplicated frames crossing a bundle boundary, but it's close and simple enough. spectatorClient?.HandleFrame(frame); } } @@ -98,5 +105,7 @@ namespace osu.Game.Rulesets.UI public abstract partial class ReplayRecorder : Component { public Func ScreenSpaceToGamefield; + + public abstract void RecordFrame(bool important); } } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 31c7c34572..aa2c740c5b 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.UI public ReplayRecorder? Recorder { + get => recorder; set { if (value == recorder) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 3f53801372..cadae8a5d3 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -113,9 +113,7 @@ namespace osu.Game.Screens.Backgrounds } b.Depth = newDepth; - b.Anchor = b.Origin = Anchor.Centre; b.FadeInFromZero(500, Easing.OutQuint); - b.ScaleTo(1.02f).ScaleTo(1, 3500, Easing.OutQuint); dimmable.Background = Background = b; } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 3ddf837acb..8c316c7b4f 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Backgrounds { case BackgroundSource.WebmSource: { - const string relative_path = @"Resource\Webm"; + const string relative_path = @"EzResources\Webm"; string dataFolderPath = storage.GetFullPath(relative_path); Debug.Assert(!string.IsNullOrEmpty(dataFolderPath)); diff --git a/osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs b/osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs index fe3a2b04a4..141c3acff4 100644 --- a/osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs +++ b/osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs @@ -36,14 +36,8 @@ namespace osu.Game.Screens.Backgrounds //下面只用于注册全局设置 GlobalConfigStore.Config = config; - var skinSettings = new EzSkinSettings(); - GlobalConfigStore.EZConfig = skinSettings; - } - - public void InjectDependencies(IReadOnlyDependencyContainer dependencies, DependencyContainer newDependencies) - { - // 在这里注册 EzSkinSettings - newDependencies.CacheAs(new EzSkinSettings()); + var ezskinSettings = new EzSkinSettings(); + GlobalConfigStore.EZConfig = ezskinSettings; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 03d600bfa2..9b679a1344 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -127,7 +127,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private void applyRotation(bool shouldSnap) { float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation); - newRotation = (newRotation - 180) % 360 + 180; + newRotation = ((newRotation + 360 + 180) % 360) - 180; + if (MathF.Abs(newRotation) == 180) + newRotation = 180; cumulativeRotation.Value = newRotation; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index e238abbb25..365a59b033 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -490,8 +490,6 @@ namespace osu.Game.Screens.Edit Mode.Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose; Mode.BindValueChanged(onModeChanged, true); - musicController.TrackChanged += onTrackChanged; - MutationTracker.InProgress.BindValueChanged(_ => { foreach (var item in saveRelatedMenuItems) @@ -503,6 +501,7 @@ namespace osu.Game.Screens.Edit { base.Dispose(isDisposing); + // redundant (should have happened via a `resetTrack()` call in `OnExiting()`), but done for safety musicController.TrackChanged -= onTrackChanged; } @@ -845,14 +844,14 @@ namespace osu.Game.Screens.Edit { base.OnEntering(e); setUpBackground(); - resetTrack(true); + setUpTrack(seekToStart: true); } public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); setUpBackground(); - clock.BindAdjustments(); + setUpTrack(); } private void setUpBackground() @@ -899,8 +898,9 @@ namespace osu.Game.Screens.Edit beatmap.EditorTimestamp = clock.CurrentTime; }); + // `resetTrack()` MUST happen before `refetchBeatmap()`, because along other things, `refetchBeatmap()` causes a global working beatmap change, + // which would cause `EditorClock` to reload the track and automatically reapply adjustments to it if not preceded by `resetTrack()`. resetTrack(); - refetchBeatmap(); return base.OnExiting(e); @@ -909,12 +909,11 @@ namespace osu.Game.Screens.Edit public override void OnSuspending(ScreenTransitionEvent e) { base.OnSuspending(e); - clock.Stop(); + + // `resetTrack()` MUST happen before `refetchBeatmap()`, because along other things, `refetchBeatmap()` causes a global working beatmap change, + // which would cause `EditorClock` to reload the track and automatically reapply adjustments to it if not preceded by `resetTrack()`. + resetTrack(); refetchBeatmap(); - // unfortunately ordering matters here. - // this unbind MUST happen after `refetchBeatmap()`, because along other things, `refetchBeatmap()` causes a global working beatmap change, - // which causes `EditorClock` to reload the track and automatically reapply adjustments to it. - clock.UnbindAdjustments(); } private void refetchBeatmap() @@ -1038,7 +1037,7 @@ namespace osu.Game.Screens.Edit editorBeatmap.PreviewTime.Value = (int)clock.CurrentTime; } - private void resetTrack(bool seekToStart = false) + private void setUpTrack(bool seekToStart = false) { clock.Stop(); @@ -1059,6 +1058,16 @@ namespace osu.Game.Screens.Edit clock.Seek(Math.Max(0, targetTime)); } + + clock.BindAdjustments(); + musicController.TrackChanged += onTrackChanged; + } + + private void resetTrack() + { + clock.Stop(); + clock.UnbindAdjustments(); + musicController.TrackChanged -= onTrackChanged; } private void onModeChanged(ValueChangedEvent e) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 60cb3ba07c..820b31c032 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -14,6 +14,7 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -75,6 +76,11 @@ namespace osu.Game.Screens.Edit.GameplayTest foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time)) { var judgement = hitObject.Judgement; + // this is very dodgy because there's no guarantee that `JudgementResult` is the correct result type for the object. + // however, instantiating the correct one is difficult here, because `JudgementResult`s are constructed by DHOs + // and because of pooling we don't *have* a DHO to use here. + // this basically mostly attempts to fill holes in `ScoreProcessor` tallies + // so that gameplay can actually complete at the end of the map when entering gameplay test midway through it, and not much else. var result = new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult, @@ -109,30 +115,28 @@ namespace osu.Game.Screens.Edit.GameplayTest return; } - foreach (var drawableObjectEntry in enumerateDrawableEntries( - DrawableRuleset.Playfield.AllHitObjects - .Select(ho => ho.Entry) - .Where(e => e != null) - .Cast(), editorState.Time)) + foreach (var drawableObject in enumerateDrawableObjects(DrawableRuleset.Playfield.AllHitObjects, editorState.Time)) { - drawableObjectEntry.Result = new JudgementResult(drawableObjectEntry.HitObject, drawableObjectEntry.HitObject.Judgement) - { - Type = drawableObjectEntry.HitObject.Judgement.MaxResult - }; + if (drawableObject.Entry == null) + continue; + + var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); + result.Type = result.Judgement.MaxResult; + drawableObject.Entry.Result = result; } - static IEnumerable enumerateDrawableEntries(IEnumerable entries, double cutoffTime) + static IEnumerable enumerateDrawableObjects(IEnumerable drawableObjects, double cutoffTime) { - foreach (var entry in entries) + foreach (var drawableObject in drawableObjects) { - foreach (var nested in enumerateDrawableEntries(entry.NestedEntries, cutoffTime)) + foreach (var nested in enumerateDrawableObjects(drawableObject.NestedHitObjects, cutoffTime)) { if (nested.HitObject.GetEndTime() < cutoffTime) yield return nested; } - if (entry.HitObject.GetEndTime() < cutoffTime) - yield return entry; + if (drawableObject.HitObject.GetEndTime() < cutoffTime) + yield return drawableObject; } } } diff --git a/osu.Game/Screens/EzSkinSettings.cs b/osu.Game/Screens/EzSkinSettings.cs index b9b791c423..af7f609a48 100644 --- a/osu.Game/Screens/EzSkinSettings.cs +++ b/osu.Game/Screens/EzSkinSettings.cs @@ -1,11 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.IO; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Graphics; @@ -13,6 +21,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Settings; using osu.Game.Screens.Edit.Components; using osu.Game.Skinning; +using osu.Game.Skinning.Components; using osuTK; using osuTK.Graphics; @@ -20,17 +29,30 @@ namespace osu.Game.Screens { public partial class EzSkinSettings : EditorSidebarSection { + public BindableNumber? NonSquareNoteHeight; public Bindable? VirtualHitPosition; private Bindable? columnWidth; private Bindable? specialFactor; - private readonly Bindable dynamicTracking = new Bindable(false); + private readonly Bindable dynamicTracking = new Bindable(); + + private readonly Bindable globalTextureName = new Bindable((EzSelectorNameSet)4); + + private readonly Bindable selectedNoteSet = new Bindable(); + + private readonly List availableNoteSets = new List(); [Resolved] private OsuConfigManager config { get; set; } = null!; + [Resolved] + private EzSkinSettingsManager ezSkinConfig { get; set; } = null!; + [Resolved] private SkinManager skinManager { get; set; } = null!; + [Resolved] + private Storage storage { get; set; } = null!; + public EzSkinSettings() : base("EZ Skin Settings") { @@ -43,25 +65,53 @@ namespace osu.Game.Screens specialFactor = config.GetBindable(OsuSetting.SpecialFactor); VirtualHitPosition = config.GetBindable(OsuSetting.VirtualHitPosition); + var configBindable = ezSkinConfig.GetBindable(EzSkinSetting.NonSquareNoteHeight); + + NonSquareNoteHeight = new BindableNumber + { + Value = configBindable.Value, + MinValue = 1, + MaxValue = 100, + Precision = 1f, + }; + + NonSquareNoteHeight.ValueChanged += e => configBindable.Value = e.NewValue; + configBindable.ValueChanged += e => NonSquareNoteHeight.Value = e.NewValue; + // NonSquareNoteHeight.ValueChanged += onSettingsValueChanged; + + globalTextureName.Value = (EzSelectorNameSet)ezSkinConfig.GetBindable(EzSkinSetting.GlobalTextureName).Value; + globalTextureName.ValueChanged += onTextureNameChanged; + + dynamicTracking.BindTo(ezSkinConfig.GetBindable(EzSkinSetting.DynamicTracking)); dynamicTracking.ValueChanged += tracking => { - // 根据开关状态取消或添加监听器 if (tracking.NewValue) { - // 启用动态追踪,添加监听器 - columnWidth!.ValueChanged += OnSettingsValueChanged; - specialFactor!.ValueChanged += OnSettingsValueChanged; - VirtualHitPosition!.ValueChanged += OnSettingsValueChanged; + columnWidth!.ValueChanged += onSettingsValueChanged; + specialFactor!.ValueChanged += onSettingsValueChanged; + VirtualHitPosition!.ValueChanged += onSettingsValueChanged; } else { - // 禁用动态追踪,移除监听器 - columnWidth!.ValueChanged -= OnSettingsValueChanged; - specialFactor!.ValueChanged -= OnSettingsValueChanged; - VirtualHitPosition!.ValueChanged -= OnSettingsValueChanged; + columnWidth!.ValueChanged -= onSettingsValueChanged; + specialFactor!.ValueChanged -= onSettingsValueChanged; + VirtualHitPosition!.ValueChanged -= onSettingsValueChanged; } + + ezSkinConfig.SetValue(EzSkinSetting.DynamicTracking, tracking.NewValue); }; + loadAvailableNoteSets(); + + // 从配置中加载上次选择的note套图,如果有的话 + string configuredNoteSet = ezSkinConfig.Get(EzSkinSetting.NoteSetName); + if (!string.IsNullOrEmpty(configuredNoteSet) && availableNoteSets.Contains(configuredNoteSet)) + selectedNoteSet.Value = configuredNoteSet; + else if (availableNoteSets.Count > 0) + selectedNoteSet.Value = availableNoteSets[1]; + + selectedNoteSet.ValueChanged += onNoteSetChanged; + Children = new Drawable[] { new FillFlowContainer @@ -96,22 +146,57 @@ namespace osu.Game.Screens Current = VirtualHitPosition, KeyboardStep = 0.1f, }, + new EzGlobalTextureNameSelector + { + LabelText = "(全局纹理名称)Global Texture Name", + Current = globalTextureName, + TooltipText = "统一修改当前皮肤中所有组件的纹理名称" + }, + new SettingsDropdown + { + LabelText = "(Note套图)Note Set", + Current = selectedNoteSet, + Items = availableNoteSets, + TooltipText = "选择不同的Note套图,影响音符和打击光效果" + }, + new SettingsSlider + { + LabelText = "(note高)Note Height", + Current = NonSquareNoteHeight, + KeyboardStep = 0.1f, + }, new SettingsButton { Action = RefreshSkin, - }.WithTwoLineText("(刷新并保存皮肤)", "Refresh & Save Skin") + }.WithTwoLineText("(刷新&保存皮肤)", "Refresh & Save Skin") } } }; } - private void OnSettingsValueChanged(ValueChangedEvent _) => ScheduleRefresh(); + #region 追踪处理 + + private void onTextureNameChanged(ValueChangedEvent textureName) + { + updateAllSkinComponentsTextureNames(textureName.NewValue); + + ezSkinConfig.SetValue(EzSkinSetting.GlobalTextureName, (int)textureName.NewValue); + } + + private void onNoteSetChanged(ValueChangedEvent e) + { + ezSkinConfig.SetValue(EzSkinSetting.NoteSetName, e.NewValue); + + if (dynamicTracking.Value) + { + ScheduleRefresh(); + } + } private ScheduledDelegate? scheduledRefresh; - /// - /// 使用防抖技术,延迟刷新皮肤,避免滑动滑块时频繁刷新导致卡顿 - /// + private void onSettingsValueChanged(ValueChangedEvent _) => ScheduleRefresh(); + public void ScheduleRefresh() { scheduledRefresh?.Cancel(); @@ -122,8 +207,105 @@ namespace osu.Game.Screens { skinManager.CurrentSkinInfo.TriggerChange(); } + + #endregion + + #region MyRegion + + private void updateAllSkinComponentsTextureNames(EzSelectorNameSet textureName) + { + var scoreTexts = this.ChildrenOfType().ToList(); + var comboTexts = this.ChildrenOfType().ToList(); + + if (Parent != null) + { + var parentScoreTexts = Parent.ChildrenOfType().ToList(); + var parentComboTexts = Parent.ChildrenOfType().ToList(); + + scoreTexts.AddRange(parentScoreTexts); + comboTexts.AddRange(parentComboTexts); + } + + var root = this as Drawable; + + while (root.Parent != null) + { + root = root.Parent; + } + + var rootScoreTexts = root.ChildrenOfType().ToList(); + var rootComboTexts = root.ChildrenOfType().ToList(); + + scoreTexts.AddRange(rootScoreTexts); + comboTexts.AddRange(rootComboTexts); + + var hitResultScores = this.ChildrenOfType().ToList(); + + if (Parent != null) + { + var parentHitResultScores = Parent.ChildrenOfType().ToList(); + hitResultScores.AddRange(parentHitResultScores); + } + + var rootHitResultScores = root.ChildrenOfType().ToList(); + hitResultScores.AddRange(rootHitResultScores); + + foreach (var scoreText in scoreTexts) + { + scoreText.FontName.Value = textureName; + } + + foreach (var comboText in comboTexts) + { + comboText.FontName.Value = textureName; + } + + foreach (var hitResultScore in hitResultScores) + { + hitResultScore.NameDropdown.Value = textureName; + } + } + + #endregion + + private void loadAvailableNoteSets() + { + availableNoteSets.Clear(); + + try + { + const string relative_path = @"EzResources\note"; + string dataFolderPath = storage.GetFullPath(relative_path); + Debug.Assert(!string.IsNullOrEmpty(dataFolderPath)); + + if (!Directory.Exists(dataFolderPath)) + { + Directory.CreateDirectory(dataFolderPath); + Logger.Log($"EzSkinSettings create Note Path: {dataFolderPath}"); + } + + // 获取所有子文件夹作为note套图选项 + string[] directories = Directory.GetDirectories(dataFolderPath); + + foreach (string dir in directories) + { + string dirName = Path.GetFileName(dir); + availableNoteSets.Add(dirName); + } + + Logger.Log($"EzSkinSettings Find {dataFolderPath} to {availableNoteSets.Count} Note Sets", LoggingTarget.Runtime, LogLevel.Debug); + } + catch (Exception ex) + { + Logger.Error(ex, "EzSkinSettings Load NoteSets Error"); + } + } } + public partial class EzGlobalTextureNameSelector : EzSelectorEnumList; + + #region 拓展按钮的多行文本显示 + public static class SettingsButtonExtensions { public static SettingsButton WithTwoLineText(this SettingsButton button, string topText, string bottomText, int fontSize = 14) @@ -173,9 +355,5 @@ namespace osu.Game.Screens } } - public enum EditorMode - { - Default, - EzSettings - } + #endregion } diff --git a/osu.Game/Screens/Footer/ScreenFooterButton.cs b/osu.Game/Screens/Footer/ScreenFooterButton.cs index 6385901db7..d0532273bc 100644 --- a/osu.Game/Screens/Footer/ScreenFooterButton.cs +++ b/osu.Game/Screens/Footer/ScreenFooterButton.cs @@ -54,6 +54,7 @@ namespace osu.Game.Screens.Footer public LocalisableString Text { + get => text.Text; set => text.Text = value; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs index 4be6e3b8c4..a2b9db5a06 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPositionDisplay.cs @@ -158,13 +158,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; } - float relativePosition = (float)(position.Value.Value - 1) / scores.Count; + float relativePosition = Math.Clamp((float)(position.Value.Value - 1) / Math.Max(scores.Count - 1, 1), 0, 1); positionText.Current.Value = position.Value.Value; positionText.FadeTo(min_alpha + (max_alpha - min_alpha) * (1 - relativePosition), 1000, Easing.OutPow10); localPlayerMarker.FadeIn(); - localPlayerMarker.MoveToX(marker_size / 2 + Math.Min(relativePosition * (width - marker_size / 2), width - marker_size / 2), 1000, Easing.OutPow10); + float markerWidth = Math.Max(marker_size, width / scores.Count); + localPlayerMarker.ResizeWidthTo(markerWidth, 1000, Easing.OutPow10); + localPlayerMarker.MoveToX(markerWidth / 2 + (width - markerWidth) * relativePosition, 1000, Easing.OutPow10); } private partial class PositionCounter : RollingCounter diff --git a/osu.Game/Screens/Ranking/LAsAnalysisOptionsPopover.cs b/osu.Game/Screens/Ranking/LAsAnalysisOptionsPopover.cs index 74855953f9..b4d7011e89 100644 --- a/osu.Game/Screens/Ranking/LAsAnalysisOptionsPopover.cs +++ b/osu.Game/Screens/Ranking/LAsAnalysisOptionsPopover.cs @@ -10,12 +10,12 @@ namespace osu.Game.Screens.Ranking { public partial class LAsAnalysisOptionsPopover : OsuPopover { - private readonly BeatmapInfo beatmapInfo; + // private readonly BeatmapInfo beatmapInfo; public LAsAnalysisOptionsPopover(BeatmapInfo beatmapInfo) : base(false) { - this.beatmapInfo = beatmapInfo; + // this.beatmapInfo = beatmapInfo; Body.CornerRadius = 4; } diff --git a/osu.Game/Screens/Select/Filter/ManiaRulesetDropdown.cs b/osu.Game/Screens/Select/Filter/ManiaRulesetDropdown.txt similarity index 100% rename from osu.Game/Screens/Select/Filter/ManiaRulesetDropdown.cs rename to osu.Game/Screens/Select/Filter/ManiaRulesetDropdown.txt diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 4c70b8c58f..08481547c0 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -26,6 +26,11 @@ namespace osu.Game.Screens.SelectV2 { public Action? RequestPresentBeatmap { private get; init; } + /// + /// From the provided beatmaps, return the most appropriate one for the user's skill. + /// + public Func, BeatmapInfo>? ChooseRecommendedBeatmap { private get; init; } + public const float SPACING = 3f; private IBindableList detachedBeatmaps = null!; @@ -181,8 +186,13 @@ namespace osu.Game.Screens.SelectV2 return; case BeatmapSetInfo setInfo: - // Selecting a set isn't valid – let's re-select the first difficulty. - CurrentSelection = setInfo.Beatmaps.First(); + // Selecting a set isn't valid – let's re-select the first visible difficulty. + if (grouping.SetItems.TryGetValue(setInfo, out var items)) + { + var beatmaps = items.Select(i => i.Model).OfType(); + CurrentSelection = ChooseRecommendedBeatmap?.Invoke(beatmaps) ?? beatmaps.First(); + } + return; case BeatmapInfo beatmapInfo: diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs index ee213f1e93..545fbbd5fd 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterMatching.cs @@ -41,17 +41,20 @@ namespace osu.Game.Screens.SelectV2 { var beatmap = (BeatmapInfo)item.Model; - if (checkMatch(beatmap, criteria)) - { - countMatching++; - yield return item; - } + if (beatmap.Hidden) + continue; + + if (!checkCriteriaMatch(beatmap, criteria)) + continue; + + countMatching++; + yield return item; } BeatmapItemsCount = countMatching; } - private static bool checkMatch(BeatmapInfo beatmap, FilterCriteria criteria) + private static bool checkCriteriaMatch(BeatmapInfo beatmap, FilterCriteria criteria) { bool match = criteria.Ruleset == null || beatmap.Ruleset.ShortName == criteria.Ruleset.ShortName || diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs index 036bacb5e9..0db4ce8aec 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -62,6 +62,7 @@ namespace osu.Game.Screens.SelectV2 private Container personalBestDisplay = null!; private Container personalBestScoreContainer = null!; + private OsuSpriteText personalBestText = null!; private LoadingLayer loading = null!; private CancellationTokenSource? cancellationTokenSource; @@ -129,10 +130,9 @@ namespace osu.Game.Screens.SelectV2 Padding = new MarginPadding { Top = 5f, Bottom = 5f, Left = 70f, Right = 10f }, Children = new Drawable[] { - new OsuSpriteText + personalBestText = new OsuSpriteText { Colour = colourProvider.Content2, - Text = "Personal Best", Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), }, personalBestScoreContainer = new Container @@ -189,7 +189,7 @@ namespace osu.Game.Screens.SelectV2 private void refetchScores() { - SetScores(Array.Empty(), null); + SetScores(Array.Empty()); if (beatmap.IsDefault) { @@ -225,10 +225,10 @@ namespace osu.Game.Screens.SelectV2 if (scores.FailState != null) SetState((LeaderboardState)scores.FailState); else - SetScores(scores.TopScores, scores.UserScore); + SetScores(scores.TopScores, scores.UserScore, scores.TotalScores); } - protected void SetScores(IEnumerable scores, ScoreInfo? userScore) + protected void SetScores(IEnumerable scores, ScoreInfo? userScore = null, int? totalCount = null) { cancellationTokenSource?.Cancel(); cancellationTokenSource = new CancellationTokenSource(); @@ -287,6 +287,11 @@ namespace osu.Game.Screens.SelectV2 }; scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding { Bottom = personal_best_height }, 300, Easing.OutQuint); + + if (totalCount != null && userScore.Position != null) + personalBestText.Text = $"Personal Best (#{userScore.Position:N0} of {totalCount.Value:N0})"; + else + personalBestText.Text = "Personal Best"; } } diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index da9d5fe89b..ffd1418796 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -338,6 +338,7 @@ namespace osu.Game.Screens.SelectV2 { genre.Data = null; language.Data = null; + userTags.Tags = null; return; } diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs index 897349b9cb..a98c806634 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs @@ -56,9 +56,15 @@ namespace osu.Game.Screens.SelectV2 } } - public (string[] tags, Action linkAction) Tags + public (string[] tags, Action linkAction)? Tags { - set => setTags(value.tags, value.linkAction); + set + { + if (value != null) + setTags(value.Value.tags, value.Value.linkAction); + else + setLoading(); + } } public MetadataDisplay(LocalisableString label) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs index 20c27dba92..2d1b412289 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmap.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -96,7 +96,7 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Both, Children = new Drawable[] { - starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -190,6 +190,8 @@ namespace osu.Game.Screens.SelectV2 localRank.Beatmap = null; starDifficultyBindable = null; + + starDifficultyCancellationSource?.Cancel(); } private void computeStarRating() @@ -202,10 +204,21 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)Item.Model; - starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, 200); starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true); } + protected override void Update() + { + base.Update(); + + if (Item?.IsVisible != true) + { + starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource = null; + } + } + private void updateKeyCount() { if (Item == null) diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs index 7f5aa6ffe8..23afe96133 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -102,6 +102,7 @@ namespace osu.Game.Screens.SelectV2 Anchor = Anchor.CentreLeft, TextSize = OsuFont.Style.Caption2.Size, Margin = new MarginPadding { Right = 5f }, + Animated = false, }, difficultiesDisplay = new DifficultySpectrumDisplay { diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs index 9a61ce998c..c59f1d9c80 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.SelectV2 AutoSizeAxes = Axes.Both, Children = new Drawable[] { - difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true) { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, @@ -226,6 +226,8 @@ namespace osu.Game.Screens.SelectV2 updateButton.BeatmapSet = null; difficultyRank.Beatmap = null; starDifficultyBindable = null; + + starDifficultyCancellationSource?.Cancel(); } private void computeStarRating() @@ -238,10 +240,21 @@ namespace osu.Game.Screens.SelectV2 var beatmap = (BeatmapInfo)Item.Model; - starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, 200); starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true); } + protected override void Update() + { + base.Update(); + + if (Item?.IsVisible != true) + { + starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource = null; + } + } + private void updateKeyCount() { if (Item == null) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index ba93403036..92a8c20b78 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -90,6 +90,9 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private ManageCollectionsDialog? collectionsDialog { get; set; } + [Resolved] + private DifficultyRecommender? difficultyRecommender { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -162,6 +165,7 @@ namespace osu.Game.Screens.SelectV2 BleedTop = FilterControl.HEIGHT_FROM_SCREEN_TOP + 5, BleedBottom = ScreenFooter.HEIGHT + 5, RequestPresentBeatmap = _ => OnStart(), + ChooseRecommendedBeatmap = getRecommendedBeatmap, NewItemsPresented = newItemsPresented, RelativeSizeAxes = Axes.Both, }, @@ -228,6 +232,13 @@ namespace osu.Game.Screens.SelectV2 detailsArea.Height = wedgesContainer.DrawHeight - titleWedge.LayoutSize.Y - 4; } + #region Selection handling + + private BeatmapInfo getRecommendedBeatmap(IEnumerable beatmaps) + => difficultyRecommender?.GetRecommendedBeatmap(beatmaps) ?? beatmaps.First(); + + #endregion + #region Transitions public override void OnEntering(ScreenTransitionEvent e) @@ -240,6 +251,7 @@ namespace osu.Game.Screens.SelectV2 detailsArea.Show(); filterControl.Show(); + modSelectOverlay.Beatmap.BindTo(Beatmap); modSelectOverlay.SelectedMods.BindTo(Mods); } @@ -255,6 +267,8 @@ namespace osu.Game.Screens.SelectV2 detailsArea.Show(); filterControl.Show(); + modSelectOverlay.Beatmap.BindTo(Beatmap); + // required due to https://github.com/ppy/osu-framework/issues/3218 modSelectOverlay.SelectedMods.Disabled = false; modSelectOverlay.SelectedMods.BindTo(Mods); @@ -265,6 +279,7 @@ namespace osu.Game.Screens.SelectV2 this.FadeOut(fade_duration, Easing.OutQuint); modSelectOverlay.SelectedMods.UnbindFrom(Mods); + modSelectOverlay.Beatmap.UnbindFrom(Beatmap); titleWedge.Hide(); detailsArea.Hide(); @@ -342,7 +357,6 @@ namespace osu.Game.Screens.SelectV2 filterDebounce?.Cancel(); filterDebounce = Scheduler.AddDelayed(() => { - noResultsPlaceholder.Filter = criteria; carousel.Filter(criteria); }, filter_delay); } @@ -351,7 +365,13 @@ namespace osu.Game.Screens.SelectV2 { int count = carousel.MatchedBeatmapsCount; - noResultsPlaceholder.State.Value = count == 0 ? Visibility.Visible : Visibility.Hidden; + if (count == 0) + { + noResultsPlaceholder.Show(); + noResultsPlaceholder.Filter = carousel.Criteria; + } + else + noResultsPlaceholder.Hide(); // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitResultScore.cs b/osu.Game/Skinning/Components/EzComHitResultScore.cs similarity index 88% rename from osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitResultScore.cs rename to osu.Game/Skinning/Components/EzComHitResultScore.cs index 8eaabc541c..23aec694a6 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitResultScore.cs +++ b/osu.Game/Skinning/Components/EzComHitResultScore.cs @@ -13,12 +13,10 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.JudgementCounter; -using osu.Game.Skinning; -using osu.Game.Skinning.Components; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD +namespace osu.Game.Skinning.Components { public partial class EzComHitResultScore : CompositeDrawable, ISerialisableDrawable, IPreviewable //, IAnimatableJudgement { @@ -32,8 +30,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD Precision = 1f, }; - [SettingSource("HitResult Text Font", "HitResult Text Font", SettingControlType = typeof(EzEnumListSelector))] - public Bindable NameDropdown { get; } = new Bindable((OffsetNumberName)49); + [SettingSource("HitResult Text Font", "HitResult Text Font", SettingControlType = typeof(EzSelectorEnumList))] + public Bindable NameDropdown { get; } = new Bindable((EzSelectorNameSet)49); // private EzComsPreviewOverlay previewOverlay = null!; // private IconButton previewButton = null!; @@ -60,9 +58,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD [Resolved] private TextureStore textures { get; set; } = null!; - // [Resolved] - // private OsuGame game { get; set; } = null!; - [Resolved] private ScoreProcessor processor { get; set; } = null!; @@ -124,43 +119,43 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD // private void showPreview() => previewOverlay.Show(); - public Drawable CreatePreviewDrawable(OffsetNumberName name) - { - var container = new Container - { - AutoSizeAxes = Axes.Both, - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10), - Children = new[] - { - createPreviewSprite(name, HitResult.Perfect), - createPreviewSprite(name, HitResult.Great), - createPreviewSprite(name, HitResult.Good), - createPreviewSprite(name, HitResult.Ok), - createPreviewSprite(name, HitResult.Meh), - createPreviewSprite(name, HitResult.Miss) - } - } - }; - - return container; - } - - private Sprite createPreviewSprite(OffsetNumberName name, HitResult result) - { - string basePath = $@"EzResources/enumBase/enumJudgement/{name}"; - var texture = textures.Get($"{basePath}.png"); - - return new Sprite - { - Texture = texture, - Size = new Vector2(50), - Alpha = 1 - }; - } + // public Drawable CreatePreviewDrawable(EzSelectorNameSet name) + // { + // var container = new Container + // { + // AutoSizeAxes = Axes.Both, + // Child = new FillFlowContainer + // { + // AutoSizeAxes = Axes.Both, + // Direction = FillDirection.Horizontal, + // Spacing = new Vector2(10), + // Children = new[] + // { + // createPreviewSprite(name, HitResult.Perfect), + // createPreviewSprite(name, HitResult.Great), + // createPreviewSprite(name, HitResult.Good), + // createPreviewSprite(name, HitResult.Ok), + // createPreviewSprite(name, HitResult.Meh), + // createPreviewSprite(name, HitResult.Miss) + // } + // } + // }; + // + // return container; + // } + // + // private Sprite createPreviewSprite(EzSelectorNameSet name, HitResult result) + // { + // string basePath = $@"EzResources/enumBase/enumJudgement/{name}"; + // var texture = textures.Get($"{basePath}.png"); + // + // return new Sprite + // { + // Texture = texture, + // Size = new Vector2(50), + // Alpha = 1 + // }; + // } protected override void LoadComplete() { @@ -314,11 +309,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD if (missCounter.ResultCount.Value == 0 && fullComboSprite != null) { - // 显示 FULL COMBO 贴图 fullComboSprite.Alpha = 1; fullComboSprite.FadeIn(50).Then().FadeOut(5000); - // 播放音效 fullComboSound?.Play(); } } @@ -334,7 +327,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD if (!drawable.IsLoaded) return; - // 为每种判定结果定义颜色数组 var colors = hitResult switch { HitResult.Miss => new[] { Color4.Red, Color4.IndianRed }, @@ -419,9 +411,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD { // 先结束之前的所有变换 drawable.FinishTransforms(); - // 固定拉长压扁比例 + var finalScale = new Vector2(1.5f, 0.05f); - // 固定动画持续时间 + const double scale_up_duration = 150; // 放大动画 const double scale_down_duration = 180; // 压扁动画 const double fade_out_duration = scale_down_duration + 10; // 淡出动画 @@ -441,6 +433,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD .FadeOut(fade_out_duration, Easing.InQuint); } + #region Other + protected virtual void Clear() { FinishTransforms(true); @@ -477,5 +471,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD { isDragging = false; } + + #endregion } } diff --git a/osu.Game/Skinning/Components/EzComScoreCounter.cs b/osu.Game/Skinning/Components/EzComScoreCounter.cs index 76c725c6a0..8c969b0efb 100644 --- a/osu.Game/Skinning/Components/EzComScoreCounter.cs +++ b/osu.Game/Skinning/Components/EzComScoreCounter.cs @@ -16,8 +16,8 @@ namespace osu.Game.Skinning.Components { protected override double RollingDuration => 250; - [SettingSource("Font", "Font", SettingControlType = typeof(EzEnumListSelector))] - public Bindable FontNameDropdown { get; } = new Bindable((OffsetNumberName)Enum.ToObject(typeof(OffsetNumberName), 43)); + [SettingSource("Font", "Font", SettingControlType = typeof(EzSelectorEnumList))] + public Bindable FontNameDropdown { get; } = new Bindable((EzSelectorNameSet)Enum.ToObject(typeof(EzSelectorNameSet), 43)); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] public Bindable ShowLabel { get; } = new BindableBool(); diff --git a/osu.Game/Skinning/Components/EzComboText.cs b/osu.Game/Skinning/Components/EzComboText.cs index bf9b917bbe..58ef7879c4 100644 --- a/osu.Game/Skinning/Components/EzComboText.cs +++ b/osu.Game/Skinning/Components/EzComboText.cs @@ -13,7 +13,7 @@ namespace osu.Game.Skinning.Components public partial class EzComboText : CompositeDrawable, IHasText { public readonly EzGetComboTexture TextPart; - public Bindable FontName { get; } = new Bindable((OffsetNumberName)4); + public Bindable FontName { get; } = new Bindable((EzSelectorNameSet)4); public FillFlowContainer TextContainer { get; private set; } @@ -27,7 +27,7 @@ namespace osu.Game.Skinning.Components // public object Spacing { get; set; } - public EzComboText(Bindable? externalFontName = null) + public EzComboText(Bindable? externalFontName = null) { AutoSizeAxes = Axes.Both; Anchor = Anchor.Centre; diff --git a/osu.Game/Skinning/Components/EzEnumListSelector.cs b/osu.Game/Skinning/Components/EzEnumListSelector.cs deleted file mode 100644 index dd647a2eeb..0000000000 --- a/osu.Game/Skinning/Components/EzEnumListSelector.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Settings; - -namespace osu.Game.Skinning.Components -{ - public partial class EzEnumListSelector : SettingsDropdown - { - protected override void LoadComplete() - { - base.LoadComplete(); - - Items = Enum.GetValues(typeof(OffsetNumberName)).Cast().ToList(); - } - } - - public partial class AnchorDropdown : SettingsDropdown - { - protected override void LoadComplete() - { - base.LoadComplete(); - - // 限制选项范围 - Items = new List - { - Anchor.TopCentre, - Anchor.Centre, - Anchor.BottomCentre - }; - } - } - - public enum EffectType - { - Scale, - Bounce, - None - } - - public enum OffsetNumberName - { - // ReSharper disable InconsistentNaming - EZ2DJ_1st, - EZ2DJ_1stSE, - EZ2DJ_2nd, - EZ2DJ_3rd, - EZ2DJ_4th, - EZ2DJ_6th, - EZ2DJ_7th, - AIR, - AZURE_EXPRESSION, - CV_CRAFT, - D2D_Station, - Dark_Concert, - DJMAX, - EC_1304, - EC_Wheel, - EVOLVE, - EZ2ON, - FIND_A_WAY, - Fortress2, - Fortress3_Future, - Fortress3_Gear, - Fortress3_Green, - Fortress3_Modern, - GC, - GC_EZ, - Gem, - HX_1121, - HX_STANDARD, - JIYU, - Kings, - Limited, - NIGHT_FALL, - O2_A9100, - O2_EA05, - O2_Jam, - Platinum, - QTZ_01, - QTZ_02, - REBOOT, - SG_701, - SH_512, - Star, - TANOc, - TANOc2, - TECHNIKA, - TIME_TRAVELER, - TOMATO, - Turtle, - Various_Ways, - ArcadeScore, - // ReSharper restore InconsistentNaming - } - - public partial class EzSelector - { - protected virtual string SetPath => @"Gameplay/Fonts/"; - - public Bindable Selected { get; } = new Bindable(); - - private Dropdown dropdown = null!; - - [BackgroundDependencyLoader] - private void load() - { - string[] folders = Directory.GetDirectories(SetPath) - .Select(Path.GetFileName) - .Where(name => name != null) - .ToArray()!; - - InternalChild = dropdown = new OsuDropdown - { - RelativeSizeAxes = Axes.X, - Items = folders - }; - - dropdown.Current.BindTo(Selected); - - if (folders.Length > 0) - Selected.Value = folders[0]; - } - - public Dropdown InternalChild { get; set; } = null!; - } -} diff --git a/osu.Game/Skinning/Components/EzGetNoteTexture.cs b/osu.Game/Skinning/Components/EzGetNoteTexture.cs new file mode 100644 index 0000000000..c41c73434c --- /dev/null +++ b/osu.Game/Skinning/Components/EzGetNoteTexture.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Skinning.Components +{ + public partial class EzGetNoteTexture : Sprite + { + public Bindable NoteName { get; } + public Bindable ThemeName { get; } + + private readonly Func getLookup; + private NoteTextureStore textureStore = null!; + + public EzGetNoteTexture(Func getLookup, Bindable themeName, Bindable noteName) + { + this.getLookup = getLookup; + ThemeName = themeName; + NoteName = noteName; + + RelativeSizeAxes = Axes.None; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + textureStore = new NoteTextureStore(textures, getLookup); + + NoteName.BindValueChanged(_ => updateTexture(), true); + ThemeName.BindValueChanged(_ => updateTexture(), true); + } + + private void updateTexture() + { + Texture = textureStore.Get(ThemeName.Value, NoteName.Value); + } + + private class NoteTextureStore + { + private readonly TextureStore textures; + private readonly Func getLookup; + private readonly Dictionary cache = new Dictionary(); + + public NoteTextureStore(TextureStore textures, Func getLookup) + { + this.textures = textures; + this.getLookup = getLookup; + } + + public Texture? Get(string themeName, string noteName) + { + string cacheKey = $"{themeName}:{noteName}"; + + if (cache.TryGetValue(cacheKey, out var cached)) + return cached; + + string lookup = getLookup(noteName); + Texture? texture = null; + + if (!string.IsNullOrEmpty(themeName)) + { + string themeNameNormalized = themeName.Replace(" ", "_"); + + string[] possiblePaths = new[] + { + $"EzResources/GameTheme/{themeNameNormalized}/notes/{lookup}", + $"EzResources/GameTheme/{themeNameNormalized}/notes/default/{lookup}", + }; + + foreach (string path in possiblePaths) + { + texture = textures.Get(path); + if (texture != null) + break; + } + } + + cache[cacheKey] = texture; + return texture; + } + } + } +} diff --git a/osu.Game/Skinning/Components/EzHitFactory.cs b/osu.Game/Skinning/Components/EzHitFactory.cs new file mode 100644 index 0000000000..20427a1e36 --- /dev/null +++ b/osu.Game/Skinning/Components/EzHitFactory.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; +// using osu.Framework.Logging; +using osu.Game.Screens; + +namespace osu.Game.Skinning.Components +{ + public partial class EzHitFactory : CompositeDrawable + { + public Bindable TextureNameBindable { get; } = new Bindable("evolve"); + public string TextureBasePath = @"EzResources/note"; + + private readonly TextureStore textureStore; + private readonly EzSkinSettingsManager ezSkinConfig; + + private const float fps = 60; + + public EzHitFactory(TextureStore textureStore, EzSkinSettingsManager ezSkinConfig, string? customTexturePath = null) + { + this.textureStore = textureStore; + this.ezSkinConfig = ezSkinConfig; + + if (!string.IsNullOrEmpty(customTexturePath)) + TextureBasePath = customTexturePath; + + AutoSizeAxes = Axes.Both; + Blending = new BlendingParameters + { + Source = BlendingType.SrcAlpha, + Destination = BlendingType.One, + }; + + initialize(); + } + + private void initialize() + { + TextureNameBindable.Value = ezSkinConfig.Get(EzSkinSetting.NoteSetName); + + ezSkinConfig.GetBindable(EzSkinSetting.NoteSetName).BindValueChanged(e => + TextureNameBindable.Value = e.NewValue, true); + } + + [BackgroundDependencyLoader] + private void load() + { + } + + public virtual Drawable CreateAnimation(string component) + { + string noteSetName = TextureNameBindable.Value; + + var container = new Container + { + AutoSizeAxes = Axes.None, // 关闭自动尺寸 + }; + + var animation = new TextureAnimation + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.None, // 关闭相对尺寸 + // Scale = new osuTK.Vector2(2), + Loop = false + }; + + for (int i = 0;; i++) + { + string framePath = $@"{TextureBasePath}/{noteSetName}/{component}/{i:D3}.png"; + var texture = textureStore.Get(framePath); + // Logger.Log($"EzHitFactory: Try load {framePath}, result: {(texture != null ? "Success" : "Fail")}", LoggingTarget.Runtime, LogLevel.Debug); + if (texture == null) + break; + + animation.AddFrame(texture); + } + + // Logger.Log($"EzHitFactory: Animation frame count = {animation.FrameCount}", LoggingTarget.Runtime, LogLevel.Debug);Logger.Log($"EzHitFactory: Animation frame count = {animation.FrameCount}", LoggingTarget.Runtime, LogLevel.Debug); + animation.OnUpdate += _ => + { + var tex = animation.CurrentFrame?.Size; + + if (tex != null) + { + container.Width = tex.Value.X; + container.Height = tex.Value.Y; + // animation.Y = -tex.Value.Y / 8f; + } + + if (animation.CurrentFrameIndex == animation.FrameCount - 1) + animation.Expire(); + }; + + container.Add(animation); + return container; + } + } +} diff --git a/osu.Game/Skinning/Components/EzNoteContainer.cs b/osu.Game/Skinning/Components/EzNoteContainer.cs new file mode 100644 index 0000000000..3afb4318ce --- /dev/null +++ b/osu.Game/Skinning/Components/EzNoteContainer.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Skinning.Components +{ + public partial class EzNoteContainer : CompositeDrawable + { + public readonly EzGetNoteTexture NotePart; + public Bindable ThemeName { get; } = new Bindable((EzSelectorNameSet)7); + public Bindable NoteType { get; } = new Bindable("default"); + + public EzNoteContainer(Bindable? externalThemeName = null, Bindable? externalNoteType = null) + { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + if (externalThemeName is not null) + ThemeName.BindTo(externalThemeName); + + if (externalNoteType is not null) + NoteType.BindTo(externalNoteType); + + var themeNameString = new Bindable(); + ThemeName.BindValueChanged(e => themeNameString.Value = e.NewValue.ToString(), true); + + NotePart = new EzGetNoteTexture(noteLookup, themeNameString, NoteType); + + InternalChildren = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Child = NotePart + }, + }; + } + + private string noteLookup(string noteType) + { + switch (noteType) + { + case "hold": return "hold"; + case "hold_head": return "hold_head"; + case "hold_tail": return "hold_tail"; + case "hold_body": return "hold_body"; + default: return "normal"; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // 可以在这里添加缩放或其他初始化逻辑 + ThemeName.BindValueChanged(e => + { + NotePart.ThemeName.Value = e.NewValue.ToString(); + NotePart.Invalidate(); + }, true); + + NoteType.BindValueChanged(e => + { + NotePart.NoteName.Value = e.NewValue; + NotePart.Invalidate(); + }, true); + } + } +} diff --git a/osu.Game/Skinning/Components/EzScoreText.cs b/osu.Game/Skinning/Components/EzScoreText.cs index 644dd4dc13..dfaa7ecb6a 100644 --- a/osu.Game/Skinning/Components/EzScoreText.cs +++ b/osu.Game/Skinning/Components/EzScoreText.cs @@ -13,7 +13,7 @@ namespace osu.Game.Skinning.Components public partial class EzScoreText : CompositeDrawable, IHasText { public readonly EzGetScoreTexture TextPart; - public Bindable FontName { get; } = new Bindable((OffsetNumberName)7); + public Bindable FontName { get; } = new Bindable((EzSelectorNameSet)7); public FillFlowContainer TextContainer { get; private set; } @@ -27,7 +27,7 @@ namespace osu.Game.Skinning.Components // public object Spacing { get; set; } - public EzScoreText(Bindable? externalFontName = null) + public EzScoreText(Bindable? externalFontName = null) { AutoSizeAxes = Axes.Both; Anchor = Anchor.Centre; diff --git a/osu.Game/Skinning/Components/EzSelectorEnumList.cs b/osu.Game/Skinning/Components/EzSelectorEnumList.cs new file mode 100644 index 0000000000..41927c75b6 --- /dev/null +++ b/osu.Game/Skinning/Components/EzSelectorEnumList.cs @@ -0,0 +1,237 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Skinning.Components +{ + public partial class EzSelectorEnumList : SettingsDropdown + { + protected override void LoadComplete() + { + base.LoadComplete(); + + Items = Enum.GetValues(typeof(EzSelectorNameSet)).Cast().ToList(); + } + } + + public partial class AnchorDropdown : SettingsDropdown + { + protected override void LoadComplete() + { + base.LoadComplete(); + + // 限制选项范围 + Items = new List + { + Anchor.TopCentre, + Anchor.Centre, + Anchor.BottomCentre + }; + } + } + + public enum EffectType + { + Scale, + Bounce, + None + } + + public enum EzSelectorNameSet + { + // ReSharper disable InconsistentNaming + EZ2DJ_1st, + EZ2DJ_1stSE, + EZ2DJ_2nd, + EZ2DJ_3rd, + EZ2DJ_4th, + EZ2DJ_6th, + EZ2DJ_7th, + AIR, + AZURE_EXPRESSION, + CV_CRAFT, + D2D_Station, + Dark_Concert, + DJMAX, + EC_1304, + EC_Wheel, + EVOLVE, + EZ2ON, + FIND_A_WAY, + Fortress2, + Fortress3_Future, + Fortress3_Gear, + Fortress3_Green, + Fortress3_Modern, + GC, + GC_EZ, + Gem, + HX_1121, + HX_STANDARD, + JIYU, + Kings, + Limited, + NIGHT_FALL, + O2_A9100, + O2_EA05, + O2_Jam, + Platinum, + QTZ_01, + QTZ_02, + REBOOT, + SG_701, + SH_512, + Star, + TANOc, + TANOc2, + TECHNIKA, + TIME_TRAVELER, + TOMATO, + Turtle, + Various_Ways, + ArcadeScore, + // ReSharper restore InconsistentNaming + } + + public partial class EzSelector + { + /// + /// 子文件夹路径,默认为Gameplay/Fonts/ + /// + protected virtual string SetPath => @"Gameplay/Fonts/"; + + /// + /// 当前选择的子文件夹 + /// + public Bindable Selected { get; } = new Bindable(); + + private Dropdown dropdown = null!; + + /// + /// 存储服务,用于访问文件系统 + /// + [Resolved] + protected Storage Storage { get; private set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + // 加载子文件夹并创建下拉列表 + LoadSubfoldersToDropdown(); + } + + /// + /// 加载指定路径下的所有子文件夹到下拉列表 + /// + protected void LoadSubfoldersToDropdown() + { + // 确保路径存在 + EnsureDirectoryExists(SetPath); + + // 获取所有子文件夹 + List subfolders = GetSubfolders(SetPath); + + // 创建下拉列表 + InternalChild = dropdown = new OsuDropdown + { + RelativeSizeAxes = Axes.X, + Items = subfolders + }; + + dropdown.Current.BindTo(Selected); + + // 默认选择第一个子文件夹,如果有的话 + if (subfolders.Count > 0) + Selected.Value = subfolders[0]; + } + + /// + /// 确保目录存在,不存在则创建 + /// + /// 相对路径 + /// 完整路径 + protected string EnsureDirectoryExists(string relativePath) + { + try + { + string fullPath = Storage.GetFullPath(relativePath); + + if (!Directory.Exists(fullPath)) + { + Directory.CreateDirectory(fullPath); + Logger.Log($"已创建目录: {fullPath}"); + } + + return fullPath; + } + catch (Exception ex) + { + Logger.Error(ex, $"创建目录 {relativePath} 失败"); + return string.Empty; + } + } + + /// + /// 获取指定路径下的所有子文件夹 + /// + /// 相对路径 + /// 子文件夹名称列表 + protected List GetSubfolders(string relativePath) + { + List result = new List(); + + try + { + string fullPath = Storage.GetFullPath(relativePath); + + // 获取所有子文件夹 + string[] directories = Directory.GetDirectories(fullPath); + + // 提取文件夹名称 + foreach (string dir in directories) + { + string dirName = Path.GetFileName(dir); + if (!string.IsNullOrEmpty(dirName)) + result.Add(dirName); + } + + Logger.Log($"在 {fullPath} 中找到 {result.Count} 个子文件夹"); + } + catch (Exception ex) + { + Logger.Error(ex, $"获取 {relativePath} 的子文件夹时出错"); + } + + // 如果没有找到子文件夹,添加一个默认选项 + if (result.Count == 0) + result.Add("Default"); + + return result; + } + + public Dropdown InternalChild { get; set; } = null!; + } + + /// + /// 专门用于加载Note套图的选择器 + /// + public partial class NoteSetSelector : EzSelector + { + /// + /// 重写路径指向note套图目录 + /// + protected override string SetPath => @"Resource\Textures\note"; + } +} diff --git a/osu.Game/Skinning/Components/EzSelectorTextures.cs b/osu.Game/Skinning/Components/EzSelectorTextures.cs index b55ff40a91..8bf32c7314 100644 --- a/osu.Game/Skinning/Components/EzSelectorTextures.cs +++ b/osu.Game/Skinning/Components/EzSelectorTextures.cs @@ -19,13 +19,13 @@ using osuTK.Graphics; namespace osu.Game.Skinning.Components { //TODO 代码不对,无法加载 - public partial class EzSelectorTextures : SettingsItem + public partial class EzSelectorTextures : SettingsItem { - private FillFlowContainer previewList = null!; + // private FillFlowContainer previewList = null!; public EzSelectorTextures() { - Current = new Bindable((OffsetNumberName)49); + Current = new Bindable((EzSelectorNameSet)49); } protected override Drawable CreateControl() @@ -34,7 +34,7 @@ namespace osu.Game.Skinning.Components { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = previewList = new FillFlowContainer + Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -46,7 +46,7 @@ namespace osu.Game.Skinning.Components private IEnumerable createPreviewItems() { - foreach (OffsetNumberName value in Enum.GetValues(typeof(OffsetNumberName))) + foreach (EzSelectorNameSet value in Enum.GetValues(typeof(EzSelectorNameSet))) { yield return new PreviewContainer { @@ -59,7 +59,7 @@ namespace osu.Game.Skinning.Components private partial class PreviewContainer : Container { - public OffsetNumberName Value { get; set; } + public EzSelectorNameSet Value { get; set; } public Action? Action { get; set; } private Box? background; diff --git a/osu.Game/Skinning/EzStyleProSkin.cs b/osu.Game/Skinning/EzStyleProSkin.cs new file mode 100644 index 0000000000..c54b42bc29 --- /dev/null +++ b/osu.Game/Skinning/EzStyleProSkin.cs @@ -0,0 +1,252 @@ +// Copyright (c) ppy Pty Ltd . 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 JetBrains.Annotations; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Game.Audio; +using osu.Game.Beatmaps.Formats; +using osu.Game.Extensions; +using osu.Game.IO; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.JudgementCounter; +using osu.Game.Skinning.Components; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Skinning +{ + public class EzStyleProSkin : Skin + { + public static SkinInfo CreateInfo() => new SkinInfo + { + ID = Skinning.SkinInfo.EZ_STYLE_PRO_SKIN, + Name = "LA's \"Ez Style Pro\" Circle(2025)", + Creator = "SK_la", + Protected = true, + InstantiationInfo = typeof(EzStyleProSkin).GetInvariantInstantiationInfo() + }; + + protected readonly IStorageResourceProvider Resources; + + public EzStyleProSkin(IStorageResourceProvider resources) + : this(CreateInfo(), resources) + { + } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] + public EzStyleProSkin(SkinInfo skin, IStorageResourceProvider resources) + : base( + skin, + resources + ) + { + Resources = resources; + } + + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Textures?.Get(componentName, wrapModeS, wrapModeT); + + public override ISample? GetSample(ISampleInfo sampleInfo) + { + return Resources.AudioManager?.Samples.Get("Gameplay/Ez/hit.wav"); + } + + public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) + { + // Temporary until default skin has a valid hit lighting. + if ((lookup as SkinnableSprite.SpriteComponentLookup)?.LookupName == @"lighting") return Drawable.Empty(); + + switch (lookup) + { + case GlobalSkinnableContainerLookup containerLookup: + switch (containerLookup.Lookup) + { + case GlobalSkinnableContainers.SongSelect: + var songSelectComponents = new DefaultSkinComponentsContainer(c => + { + var dim = c.OfType().FirstOrDefault(); + + if (dim != null) + { + dim.Anchor = Anchor.BottomCentre; + dim.Origin = Anchor.Centre; + dim.Position = new Vector2(-80, -150); + } + }) + { + // Children = new Drawable[] + // { + // new LAsSkinCom6DimPanel(), + // } + }; + + return songSelectComponents; + + case GlobalSkinnableContainers.MainHUDComponents: + + var mainHUDComponents = new DefaultSkinComponentsContainer(container => + { + var health = container.OfType().FirstOrDefault(); + var score = container.OfType().FirstOrDefault(); + var acc = container.OfType().FirstOrDefault(); + var pps = container.OfType().FirstOrDefault(); + var songProgress = container.OfType().FirstOrDefault(); + + const float x_offset = 20; + + if (health != null) + { + health.Anchor = Anchor.BottomLeft; + health.Origin = Anchor.CentreLeft; + // health.BypassAutoSizeAxes = Axes.Y; + health.Width = 0.5f; + // health.BarHeight.Value = 0f; + // health.Height = 0.4f; + health.Rotation = -90; + health.Position = new Vector2(0, 0); + + if (score != null) + { + score.Anchor = Anchor.TopLeft; + score.Origin = Anchor.TopLeft; + score.Position = new Vector2(x_offset, 20); + score.ShowLabel.Value = false; + // score.FontNameDropdown = + } + + if (acc != null) + { + acc.Position = new Vector2(-x_offset, 20); + acc.Anchor = Anchor.TopRight; + acc.Origin = Anchor.TopRight; + } + + if (pps != null && acc != null) + { + pps.Position = new Vector2(acc.X, acc.Y + acc.DrawHeight + 10); + pps.Anchor = Anchor.TopRight; + pps.Origin = Anchor.TopRight; + pps.Scale = new Vector2(0.8f); + } + + if (songProgress != null) + { + const float padding = 10; + songProgress.Position = new Vector2(0, -padding); + songProgress.Scale = new Vector2(0.9f, 1); + } + + var attributeTexts = container.OfType().ToArray(); + + if (attributeTexts.Length >= 4) + { + var attributeText = attributeTexts[0]; + var attributeText2 = attributeTexts[1]; + var attributeText3 = attributeTexts[2]; + var attributeText4 = attributeTexts[3]; + + if (pps != null) + { + attributeText.Anchor = Anchor.TopRight; + attributeText.Origin = Anchor.TopRight; + attributeText.Position = new Vector2(-x_offset, pps.Y + pps.DrawHeight * pps.Scale.Y + 10); + attributeText.Scale = new Vector2(0.65f); + attributeText.Attribute.Value = BeatmapAttribute.StarRating; + } + + attributeText2.Anchor = Anchor.TopRight; + attributeText2.Origin = Anchor.TopRight; + attributeText2.Position = new Vector2(-x_offset, attributeText.Y + attributeText.DrawHeight * attributeText.Scale.Y + 10); + attributeText2.Scale = new Vector2(0.65f); + attributeText2.Attribute.Value = BeatmapAttribute.DifficultyName; + attributeText2.Template.Value = "{Value}"; + + if (score != null) + { + attributeText3.Anchor = Anchor.TopLeft; + attributeText3.Origin = Anchor.TopLeft; + attributeText3.Scale = new Vector2(0.65f); + attributeText3.Position = new Vector2(x_offset, score.Y + score.DrawHeight * score.Scale.Y + 10); + attributeText3.Attribute.Value = BeatmapAttribute.Artist; + } + + attributeText4.Anchor = Anchor.TopLeft; + attributeText4.Origin = Anchor.TopLeft; + attributeText4.Scale = new Vector2(0.65f); + attributeText4.Position = new Vector2(x_offset, attributeText3.Y + attributeText3.DrawHeight * attributeText3.Scale.Y + 10); + attributeText4.Attribute.Value = BeatmapAttribute.Title; + } + } + }) + { + Children = new Drawable[] + { + new EzComScoreCounter(), + new BeatmapAttributeText(), + new BeatmapAttributeText(), + new BeatmapAttributeText(), + new BeatmapAttributeText(), + + new DefaultHealthDisplay(), + new ArgonAccuracyCounter + { + WireframeOpacity = { Value = 0 }, + }, + new ArgonPerformancePointsCounter + { + WireframeOpacity = { Value = 0 }, + }, + new ArgonSongProgress(), + new JudgementCounterDisplay + { + FillMode = FillMode.Fill, + FlowDirection = { Value = Direction.Vertical }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Position = new Vector2(20, 0), + }, + } + }; + + return mainHUDComponents; + } + + return null; + } + + return base.GetDrawableComponent(lookup); + } + + public override IBindable? GetConfig(TLookup lookup) + { + switch (lookup) + { + case GlobalSkinColours global: + switch (global) + { + case GlobalSkinColours.ComboColours: + { + LogLookupDebug(this, lookup, LookupDebugType.Hit); + return SkinUtils.As(new Bindable?>(Configuration.ComboColours)); + } + } + + break; + + case SkinComboColourLookup comboColour: + LogLookupDebug(this, lookup, LookupDebugType.Hit); + return SkinUtils.As(new Bindable(getComboColour(Configuration, comboColour.ColourIndex))); + } + + LogLookupDebug(this, lookup, LookupDebugType.Miss); + return null; + } + + private static Color4 getComboColour(IHasComboColours source, int colourIndex) + => source.ComboColours![colourIndex % source.ComboColours.Count]; + } +} diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 0f02b7cea4..26b9e5b9bc 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -22,6 +22,7 @@ namespace osu.Game.Skinning internal static readonly Guid CLASSIC_SKIN = new Guid("81F02CD3-EEC6-4865-AC23-FAE26A386187"); internal static readonly Guid RANDOM_SKIN = new Guid("D39DFEFB-477C-4372-B1EA-2BCEA5FB8908"); internal static readonly Guid EZ2_SKIN = new Guid("fc372386-381d-4f8e-897a-c1d89ef39f9c"); + internal static readonly Guid EZ_STYLE_PRO_SKIN = new Guid("fc372386-381d-4f8e-897a-c1d89ef39f9d"); internal static readonly Guid SBI_SKIN = new Guid("fc372386-381d-4f8e-897a-c1d89ef39f2c"); [PrimaryKey] diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 43d981d2e7..e8c5a88c63 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -96,6 +96,7 @@ namespace osu.Game.Skinning argonSkin = new ArgonSkin(this), new ArgonProSkin(this), new Ez2Skin(this), + new EzStyleProSkin(this), new SbISkin(this), }; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 6792e6939f..ca965adb15 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -72,6 +72,8 @@ namespace osu.Game.Storyboards.Drawables { Size = Vector2.One; // 填满窗口 FillMode = FillMode.Fit; // 保持比例 + Anchor = Anchor.Centre; + Origin = Anchor.Centre; } else { @@ -139,6 +141,9 @@ namespace osu.Game.Storyboards.Drawables Height = Parent.DrawHeight; Width = Height * videoAspectRatio; } + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; } Anchor = Anchor.Centre; diff --git a/osu.sln b/osu.sln index f50806f193..d99b3238d1 100644 --- a/osu.sln +++ b/osu.sln @@ -58,6 +58,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props CodeAnalysis\osu.ruleset = CodeAnalysis\osu.ruleset + global.json = global.json osu.sln.DotSettings = osu.sln.DotSettings osu.TestProject.props = osu.TestProject.props EndProjectSection @@ -98,14 +99,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CodeAnalysis", "CodeAnalysi CodeAnalysis\osu.globalconfig = CodeAnalysis\osu.globalconfig EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework", "..\osu-framework\osu.Framework\osu.Framework.csproj", "{0E2D0BC7-BFB9-4638-8F65-7F4576312ADE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework.NativeLibs", "..\osu-framework\osu.Framework.NativeLibs\osu.Framework.NativeLibs.csproj", "{3635B81B-F91C-4575-867C-6DC83CA175EA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework.Android", "..\osu-framework\osu.Framework.Android\osu.Framework.Android.csproj", "{AC291F03-507A-41BD-A40F-90C98337173A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Framework.iOS", "..\osu-framework\osu.Framework.iOS\osu.Framework.iOS.csproj", "{2F48D5D2-4576-4E64-8DF1-B1707A362D53}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Resources", "..\osu-resources\osu.Game.Resources\osu.Game.Resources.csproj", "{18ED4ABC-6064-46F1-9A05-4E0BE88A635A}" EndProject Global @@ -262,26 +255,8 @@ Global {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Debug|Any CPU.Build.0 = Debug|Any CPU {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|Any CPU.ActiveCfg = Release|Any CPU {1743BF7C-E6AE-4A06-BAD9-166D62894303}.Release|Any CPU.Build.0 = Release|Any CPU - {0E2D0BC7-BFB9-4638-8F65-7F4576312ADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E2D0BC7-BFB9-4638-8F65-7F4576312ADE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E2D0BC7-BFB9-4638-8F65-7F4576312ADE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E2D0BC7-BFB9-4638-8F65-7F4576312ADE}.Release|Any CPU.Build.0 = Release|Any CPU - {3635B81B-F91C-4575-867C-6DC83CA175EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3635B81B-F91C-4575-867C-6DC83CA175EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3635B81B-F91C-4575-867C-6DC83CA175EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3635B81B-F91C-4575-867C-6DC83CA175EA}.Release|Any CPU.Build.0 = Release|Any CPU - {AC291F03-507A-41BD-A40F-90C98337173A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AC291F03-507A-41BD-A40F-90C98337173A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AC291F03-507A-41BD-A40F-90C98337173A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AC291F03-507A-41BD-A40F-90C98337173A}.Release|Any CPU.Build.0 = Release|Any CPU - {2F48D5D2-4576-4E64-8DF1-B1707A362D53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2F48D5D2-4576-4E64-8DF1-B1707A362D53}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2F48D5D2-4576-4E64-8DF1-B1707A362D53}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2F48D5D2-4576-4E64-8DF1-B1707A362D53}.Release|Any CPU.Build.0 = Release|Any CPU {18ED4ABC-6064-46F1-9A05-4E0BE88A635A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {18ED4ABC-6064-46F1-9A05-4E0BE88A635A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {18ED4ABC-6064-46F1-9A05-4E0BE88A635A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {18ED4ABC-6064-46F1-9A05-4E0BE88A635A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/qodana.yaml b/qodana.yaml index f2bd5156e1..9744396fcf 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -27,3 +27,5 @@ profile: #Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) #plugins: # - id: #(plugin id can be found at https://plugins.jetbrains.com) +exclude: + - name: FormatStringPlaceholdersMismatch