From 58e7d2ce984d455383cca49e477746fc81231fe3 Mon Sep 17 00:00:00 2001 From: LA <1245661240@qq.com> Date: Sun, 6 Apr 2025 22:32:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E7=9A=AE=E8=82=A4=E5=88=A4=E5=AE=9A=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ManiaFilterCriteriaTest.cs | 187 +++++++++++++ .../ManiaFilterCriteria.cs | 63 ++++- .../ManiaSettingsSubsection.cs | 2 +- .../Ez2/Ez2HUD/Ez2ManiaComboCounter.cs | 106 -------- .../Skinning/Ez2/Ez2HUD/EzComComboCounter.cs | 37 ++- .../Skinning/Ez2/Ez2HUD/EzComComboSprite.cs | 251 ++++++++++++++++++ .../Ez2/Ez2HUD/EzComComboSprite2.csss | 98 +++++++ .../Skinning/Ez2/Ez2HUD/EzComComboText.cs | 61 ----- .../Skinning/Ez2/Ez2HUD/EzComCounterText.cs | 9 +- .../Skinning/Ez2/Ez2HUD/EzComHitTiming.cs | 14 +- .../Skinning/Ez2/Ez2HUD/EzComJudgementArea.cs | 184 +++++++++++++ .../Ez2/Ez2HUD/EzComJudgementTexture.cs | 226 ++++++++++++++++ ...z2ScoreCounter.cs => EzComScoreCounter.cs} | 12 +- .../Skinning/Ez2/Ez2JudgementPiece.cs | 18 +- .../Skinning/Ez2/GifJudgementPiece.cs | 158 ----------- .../Skinning/Ez2/ManiaEz2SkinTransformer.cs | 30 ++- .../Skinning/SbI/ManiaSbISkinTransformer.cs | 5 +- .../TestSceneSpinner.cs | 7 +- .../Skinning/Argon/ArgonReverseArrow.cs | 7 +- .../osu.Game.Tests.Android.csproj | 1 + osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj | 1 + .../Formats/LegacyStoryboardDecoderTest.cs | 41 +++ .../OnlinePlay/MultiplayerPlaylistItemTest.cs | 73 +++++ ...fade-transform-is-ignored-for-lifetime.osb | 8 + .../video-custom-alpha-transform.osb | 5 + .../DailyChallenge/TestSceneDailyChallenge.cs | 8 +- .../TestSceneDailyChallengeIntro.cs | 11 +- .../Editing/TestSceneEditorSamplePlayback.cs | 10 +- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 16 +- .../Visual/Gameplay/TestSceneSkinEditor.cs | 57 ++++ .../TestSceneMultiplayerMatchSubScreen.cs | 56 ++++ .../TestSceneMultiplayerPlaylist.cs | 51 ++-- .../TestSceneMultiplayerQueueList.cs | 18 +- .../Visual/Ranking/TestSceneUserTagControl.cs | 44 +-- osu.Game.Tests/osu.Game.Tests.csproj | 1 + osu.Game.Tournament/Models/TournamentMatch.cs | 1 + .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 7 +- osu.Game/Configuration/BackgroundSource.cs | 4 + .../SessionAverageHitErrorTracker.cs | 6 +- .../Database/RealmArchiveModelImporter.cs | 9 +- .../Graphics/Containers/ScalingContainer.cs | 14 +- osu.Game/Graphics/OsuFont.cs | 6 - ...trings.cs => BarHitErrorMeterStrings.csss} | 0 .../Localisation/GameplaySettingsStrings.cs | 15 -- .../HUD/BarHitErrorMeterStrings.cs | 2 - .../Localisation/LayoutSettingsStrings.cs | 5 - .../Localisation/RulesetSettingsStrings.cs | 2 +- .../Online/API/Requests/Responses/APITag.cs | 3 + osu.Game/Online/Chat/MessageNotifier.cs | 11 +- .../Online/Multiplayer/MultiplayerRoom.cs | 1 + .../Online/Rooms/MultiplayerPlaylistItem.cs | 23 ++ osu.Game/Online/Rooms/PlaylistItem.cs | 8 +- osu.Game/Overlays/OnScreenDisplay.cs | 36 +-- .../Header/Components/FollowersButton.cs | 28 +- .../Sections/Gameplay/GeneralSettings.cs | 6 +- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 9 +- .../SkinEditor/SkinSelectionHandler.cs | 105 +++++--- .../Rulesets/Judgements/LAsJudgementText.cs | 45 ++++ .../Objects/Drawables/DrawableHitObject.cs | 54 ++-- .../Rulesets/Scoring/HitEventExtensions.cs | 11 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 59 ++-- .../Backgrounds/BackgroundScreenDefault.cs | 17 ++ .../Backgrounds/VideoBackgroundScreen.cs | 30 +++ .../Screens/Edit/Timing/MetronomeDisplay.cs | 32 ++- osu.Game/Screens/Edit/Timing/TimingSection.cs | 6 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 26 +- .../Match/MultiplayerUserModSelectOverlay.cs | 124 +++++++++ .../Match/Playlist/MultiplayerPlaylist.cs | 73 ++--- .../Match/Playlist/MultiplayerQueueList.cs | 35 +-- .../Multiplayer/MultiplayerMatchSubScreen.cs | 41 +-- .../HUD/HitErrorMeters/BarHitErrorMeter.cs | 2 +- osu.Game/Screens/Play/Player.cs | 41 +-- .../PlayerSettings/BeatmapOffsetControl.cs | 18 +- .../Ranking/Statistics/AverageHitError.cs | 6 +- osu.Game/Screens/Ranking/UserTagControl.cs | 8 +- osu.Game/Screens/Select/Filter/Operator.cs | 1 + osu.Game/Screens/Select/FilterQueryParser.cs | 6 +- osu.Game/Storyboards/StoryboardSprite.cs | 39 ++- osu.Game/Storyboards/StoryboardVideo.cs | 4 +- .../OnlinePlay/TestRoomRequestsHandler.cs | 2 +- 80 files changed, 2026 insertions(+), 831 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs delete mode 100644 osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/Ez2ManiaComboCounter.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite2.csss delete mode 100644 osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboText.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComJudgementArea.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComJudgementTexture.cs rename osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/{Ez2ScoreCounter.cs => EzComScoreCounter.cs} (79%) delete mode 100644 osu.Game.Rulesets.Mania/Skinning/Ez2/GifJudgementPiece.cs create mode 100644 osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs create mode 100644 osu.Game.Tests/Resources/noop-fade-transform-is-ignored-for-lifetime.osb create mode 100644 osu.Game.Tests/Resources/video-custom-alpha-transform.osb rename osu.Game/Localisation/{BarHitErrorMeterStrings.cs => BarHitErrorMeterStrings.csss} (100%) create mode 100644 osu.Game/Rulesets/Judgements/LAsJudgementText.cs create mode 100644 osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs new file mode 100644 index 0000000000..24da447482 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class ManiaFilterCriteriaTest + { + [TestCase] + public void TestKeysEqualSingleValue() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1"); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria + { + Mods = [new ManiaModKey1()] + })); + } + + [TestCase] + public void TestKeysEqualMultipleValues() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1,3,5,7"); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria + { + Mods = [new ManiaModKey1()] + })); + } + + [TestCase] + public void TestKeysNotEqualSingleValue() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1"); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria + { + Mods = [new ManiaModKey1()] + })); + } + + [TestCase] + public void TestKeysNotEqualMultipleValues() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1,3,5,7"); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria + { + Mods = [new ManiaModKey1()] + })); + } + + [TestCase] + public void TestKeysGreaterOrEqualThan() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4"); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria + { + Mods = [new ManiaModKey7()] + })); + } + + [TestCase] + public void TestFilterIntersection() + { + var criteria = new ManiaFilterCriteria(); + criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4"); + criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "7"); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }), + new FilterCriteria())); + + Assert.False(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 7 }), + new FilterCriteria())); + + Assert.True(criteria.Matches( + new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 9 }), + new FilterCriteria())); + } + + [TestCase] + public void TestInvalidFilters() + { + var criteria = new ManiaFilterCriteria(); + + Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "some text")); + Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text")); + Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6")); + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index 8c6efbc72d..9b2700c6e8 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Mods; @@ -17,20 +18,72 @@ namespace osu.Game.Rulesets.Mania { public class ManiaFilterCriteria : IRulesetFilterCriteria { - private FilterCriteria.OptionalRange keys; + private readonly HashSet includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet(); public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) { - return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods)); + int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods); + + return includedKeyCounts.Contains(keyCount); } - public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) + public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues) { switch (key) { case "key": case "keys": - return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value); + { + var keyCounts = new HashSet(); + + foreach (string strValue in strValues.Split(',')) + { + if (!int.TryParse(strValue, out int keyCount)) + return false; + + keyCounts.Add(keyCount); + } + + int? singleKeyCount = keyCounts.Count == 1 ? keyCounts.Single() : null; + + switch (op) + { + case Operator.Equal: + includedKeyCounts.IntersectWith(keyCounts); + return true; + + case Operator.NotEqual: + includedKeyCounts.ExceptWith(keyCounts); + return true; + + case Operator.Less: + if (singleKeyCount == null) return false; + + includedKeyCounts.RemoveWhere(k => k >= singleKeyCount.Value); + return true; + + case Operator.LessOrEqual: + if (singleKeyCount == null) return false; + + includedKeyCounts.RemoveWhere(k => k > singleKeyCount.Value); + return true; + + case Operator.Greater: + if (singleKeyCount == null) return false; + + includedKeyCounts.RemoveWhere(k => k <= singleKeyCount.Value); + return true; + + case Operator.GreaterOrEqual: + if (singleKeyCount == null) return false; + + includedKeyCounts.RemoveWhere(k => k < singleKeyCount.Value); + return true; + + default: + return false; + } + } } return false; @@ -38,7 +91,7 @@ namespace osu.Game.Rulesets.Mania public bool FilterMayChangeFromMods(ValueChangedEvent> mods) { - if (keys.HasFilter) + if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT) { // Interpreting as the Mod type is required for equality comparison. HashSet oldSet = mods.OldValue.OfType().AsEnumerable().ToHashSet(); diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index cad550540d..db55eb65ef 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mania private partial class ManiaScrollBaseSpeedSlider : RoundedSliderBar { public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip( - Current.Value, 200); + (int)Current.Value, 200); } private partial class ManiaScrollTimePerSpeedSlider : RoundedSliderBar diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/Ez2ManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/Ez2ManiaComboCounter.cs deleted file mode 100644 index 0e6b393ab1..0000000000 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/Ez2ManiaComboCounter.cs +++ /dev/null @@ -1,106 +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 osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; -using osu.Game.Configuration; -using osu.Game.Localisation.SkinComponents; -using osu.Game.Resources.Localisation.Web; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Screens.Play.HUD; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD -{ - public partial class Ez2ManiaComboCounter : ComboCounter - { - protected EzComCounterText Text = null!; - - // public IBindable WireframeOpacity { get; } = new BindableFloat(); - protected override double RollingDuration => 250; - protected virtual bool DisplayXSymbol => true; - - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } = null!; - - private IBindable direction = null!; - - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] - public Bindable ShowLabel { get; } = new BindableBool(true); - - [BackgroundDependencyLoader] - private void load(ScoreProcessor scoreProcessor) - { - Current.BindTo(scoreProcessor.Combo); - Current.BindValueChanged(combo => - { - bool wasIncrease = combo.NewValue > combo.OldValue; - bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0; - - float newScale = Math.Clamp(Text.NumberContainer.Scale.X * (wasIncrease ? 3f : 1f), 0.6f, 3f); - - float duration = wasMiss ? 2000 : 500; - - Text.NumberContainer - .ScaleTo(new Vector2(newScale)) - .ScaleTo(Vector2.One, duration, Easing.OutQuint); - - if (wasMiss) - Text.FlashColour(Color4.Red, duration, Easing.OutQuint); - }); - } - - private int getDigitsRequiredForDisplayCount() - { - // one for the single presumed starting digit, one for the "x" at the end (unless disabled). - int digitsRequired = DisplayXSymbol ? 2 : 1; - long c = DisplayedCount; - while ((c /= 10) > 0) - digitsRequired++; - return digitsRequired; - } - - protected override LocalisableString FormatCount(int count) => DisplayXSymbol ? $@"{count}" : count.ToString(); - - protected override IHasText CreateText() => Text = new EzComCounterText(Anchor.TopCentre, MatchesStrings.MatchScoreStatsCombo.ToUpper()) - { - // WireframeOpacity = { BindTarget = WireframeOpacity }, - ShowLabel = { BindTarget = ShowLabel }, - }; - - protected override void LoadComplete() - { - base.LoadComplete(); - - UsesFixedAnchor = true; - - direction = scrollingInfo.Direction.GetBoundCopy(); - direction.BindValueChanged(_ => updateAnchor()); - - // 需要两个调度,以便在下一帧执行 updateAnchor, - // 这是 combo 计数器通过 Ez2ManiaSkinTransformer 的默认布局接收其 Y 位置的时间。 - Schedule(() => Schedule(updateAnchor)); - } - - private void updateAnchor() - { - // 如果锚点不是垂直中心,则根据滚动方向设置顶部或底部锚点 - if (Anchor.HasFlag(Anchor.y1)) - return; - - Anchor &= ~(Anchor.y0 | Anchor.y2); - Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0; - - // 根据滚动方向更改 Y 坐标的符号。 - // 即,如果用户将方向从下更改为上,锚点从顶部更改为底部,Y 从正数翻转为负数。 - Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1); - } - } -} diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboCounter.cs index 7fcaa5f042..bc72403913 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboCounter.cs @@ -8,8 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Localisation.SkinComponents; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; @@ -122,17 +120,38 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD private void applyBounceAnimation(bool wasIncrease, bool wasMiss) { - float factor = Math.Clamp(wasIncrease ? -10 * IncreaseScale.Value : 10 * DecreaseScale.Value, -100f, 100f); + float factor = Math.Clamp(wasIncrease ? 10 * IncreaseScale.Value : -10 * DecreaseScale.Value, -100f, 100f); Anchor originAnchor = Enum.Parse(AnimationOrigin.Value.ToString()); Text.NumberContainer.Anchor = originAnchor; Text.NumberContainer.Origin = originAnchor; + float moveToYStart = 0; + float moveToYEnd = 0; + + switch (AnimationOrigin.Value) + { + case OriginOptions.TopCentre: + moveToYStart = factor; + moveToYEnd = -5; + break; + + case OriginOptions.BottomCentre: + moveToYStart = -factor; + moveToYEnd = 5; + break; + + case OriginOptions.Centre: + moveToYStart = factor; + moveToYEnd = -5; + break; + } + Text.NumberContainer - .MoveToY(factor, IncreaseDuration.Value / 2, Easing.OutBounce) + .MoveToY(moveToYStart, IncreaseDuration.Value / 2, Easing.OutBounce) .Then() - .MoveToY(0, DecreaseDuration.Value, Easing.OutBounce); + .MoveToY(moveToYEnd, DecreaseDuration.Value, Easing.OutBounce); if (wasMiss) Text.FlashColour(Color4.Red, DecreaseDuration.Value, Easing.OutQuint); @@ -145,11 +164,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD ShowLabel = { BindTarget = ShowLabel }, }; - protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText - { - Font = OsuFont.Stat.With(size: 40f, family: "Ez"), - }; - protected override void LoadComplete() { base.LoadComplete(); @@ -162,7 +176,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD public enum AnimationType { Scale, - Bounce + Bounce, + None } public enum OriginOptions diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs new file mode 100644 index 0000000000..c076b562c2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs @@ -0,0 +1,251 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence comboSprite. + +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.Containers; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; +using Texture = osu.Framework.Graphics.Textures.Texture; + +namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD +{ + public partial class EzComComboSprite : CompositeDrawable, ISerialisableDrawable + { + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.SpriteName), nameof(SkinnableComponentStrings.SpriteNameDescription), SettingControlType = typeof(SpriteSelectorControl))] + public Bindable SpriteName { get; } = new Bindable("default"); + + [SettingSource("Animation Type", "The type of animation to apply")] + public Bindable Animation { get; } = new Bindable(AnimationType.Scale); + + [SettingSource("Increase Scale", "The scale factor when the combo increases")] + public BindableNumber IncreaseScale { get; } = new BindableNumber(0.5f) + { + MinValue = 0.1f, + MaxValue = 5f, + Precision = 0.05f, + }; + + [SettingSource("Increase Duration", "The scale duration time when the combo increases")] + public BindableNumber IncreaseDuration { get; } = new BindableNumber(10) + { + MinValue = 1, + MaxValue = 300, + Precision = 1f, + }; + + [SettingSource("Decrease Duration", "The scale duration time when the combo decrease")] + public BindableNumber DecreaseDuration { get; } = new BindableNumber(200) + { + MinValue = 10, + MaxValue = 500, + Precision = 10f, + }; + + [SettingSource("Animation Origin", "The origin point for the animation")] + public Bindable AnimationOrigin { get; } = new Bindable(OriginOptions.TopCentre); + + [SettingSource("Alpha", "The alpha value of this box")] + public BindableNumber BoxAlpha { get; } = new BindableNumber(1) + { + MinValue = 0, + MaxValue = 1, + Precision = 0.01f, + }; + + public Bindable Current { get; } = new Bindable(); + private Sprite comboSprite = null!; + private readonly Dictionary textureMap = new Dictionary(); + + [Resolved] + private TextureStore textures { get; set; } = null!; + + [Resolved] + private IRenderer renderer { get; set; } = null!; + + public EzComComboSprite(string textureName) + { + SpriteName.Value = textureName; + } + + public EzComComboSprite() + { + RelativeSizeAxes = Axes.None; + AutoSizeAxes = Axes.Both; + Anchor = Anchor.TopCentre; + Origin = Anchor.Centre; + + SpriteName.BindValueChanged(name => + { + if (IsLoaded) + updateTexture(); + }, true); + } + + private void updateTexture() + { + string lookupName = SpriteName.Value; + string localPath = Path.Combine("Skins/Ez2/combo", $"{lookupName}.png"); + + Texture? texture = null; + + Logger.Log($"Found {localPath}"); + + if (File.Exists(localPath)) + { + using (var stream = File.OpenRead(localPath)) + { + texture = Texture.FromStream(renderer, stream); + Logger.Log($"Loaded texture from {localPath}"); + } + } + + texture ??= textures.Get(@"Gameplay/Ez2/combo/default_combo.png"); + + comboSprite.Texture = texture; + } + + [BackgroundDependencyLoader] + private void load(ScoreProcessor scoreProcessor) + { + InternalChild = comboSprite = new Sprite + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre + }; + + foreach (var item in SpriteSelectorControl.SPRITE_PATH_MAP) + { + var texture = textures.Get(item.Value); + + if (texture != null) + { + textureMap[item.Key] = texture; + } + } + + Current.BindTo(scoreProcessor.Combo); + Current.BindValueChanged(combo => + { + bool wasIncrease = combo.NewValue > combo.OldValue; + bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0; + + switch (Animation.Value) + { + case AnimationType.Scale: + applyScaleAnimation(wasIncrease, wasMiss); + break; + + case AnimationType.Bounce: + applyBounceAnimation(wasIncrease, wasMiss); + break; + } + }); + + updateTexture(); + } + + private void applyScaleAnimation(bool wasIncrease, bool wasMiss) + { + float newScaleValue = Math.Clamp(comboSprite.Scale.X * (wasIncrease ? IncreaseScale.Value : 0.8f), 0.5f, 3f); + Vector2 newScale = new Vector2(newScaleValue); + + setSpriteAnchorAndOrigin(); + + comboSprite + .ScaleTo(newScale, IncreaseDuration.Value, Easing.OutQuint) + .Then() + .ScaleTo(Vector2.One, DecreaseDuration.Value, Easing.OutQuint); + + if (wasMiss) + comboSprite.FlashColour(Color4.Red, DecreaseDuration.Value, Easing.OutQuint); + } + + private void applyBounceAnimation(bool wasIncrease, bool wasMiss) + { + float factor = Math.Clamp(wasIncrease ? -10 * IncreaseScale.Value : 50, -100f, 100f); + + setSpriteAnchorAndOrigin(); + + comboSprite + .MoveToY(factor, IncreaseDuration.Value / 2, Easing.OutBounce) + .Then() + .MoveToY(0, DecreaseDuration.Value, Easing.OutBounce); + + if (wasMiss) + comboSprite.FlashColour(Color4.Red, DecreaseDuration.Value, Easing.OutQuint); + } + + private void setSpriteAnchorAndOrigin() + { + Anchor originAnchor = Enum.Parse(AnimationOrigin.Value.ToString()); + comboSprite.Anchor = originAnchor; + comboSprite.Origin = originAnchor; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + BoxAlpha.BindValueChanged(alpha => comboSprite.Alpha = alpha.NewValue, true); + } + + public bool UsesFixedAnchor { get; set; } + + public partial class SpriteSelectorControl : SettingsDropdown + { + [Resolved] + private Storage storage { get; set; } = null!; + + internal static readonly Dictionary SPRITE_PATH_MAP = new Dictionary(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + const string skin_path = @"Skins\Ez2\combo"; + SPRITE_PATH_MAP.Clear(); + SPRITE_PATH_MAP["default"] = @"Gameplay/Ez2/combo/default_combo.png"; + + string fullPath = storage.GetFullPath(skin_path); + + if (!Directory.Exists(fullPath)) + Directory.CreateDirectory(fullPath); + + string[] files = Directory.GetFiles(fullPath, "*.png", SearchOption.TopDirectoryOnly); + + foreach (string file in files) + { + string fileName = Path.GetFileNameWithoutExtension(file); + SPRITE_PATH_MAP[fileName] = Path.Combine(skin_path, Path.GetFileName(file)); + } + + Items = SPRITE_PATH_MAP.Keys.ToList(); + + Current.ValueChanged += e => + { + if (SPRITE_PATH_MAP.TryGetValue(e.NewValue, out string? path)) + { + if (Current.Value != path) + Current.Value = path; + } + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite2.csss b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite2.csss new file mode 100644 index 0000000000..6021b0718d --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite2.csss @@ -0,0 +1,98 @@ +// 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.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD +{ + public partial class EzComComboSprite2 : ComboCounter + { + private readonly IBindable direction = new Bindable(); + + private Container directionContainer = null!; + + private Drawable noteAnimation = null!; + + public EzComComboSprite2() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + InternalChild = directionContainer = new Container + { + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = noteAnimation = GetAnimation(skin) ?? Empty() + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(OnDirectionChanged, true); + } + + protected override void Update() + { + base.Update(); + + Texture? texture = null; + + if (noteAnimation is Sprite sprite) + texture = sprite.Texture; + else if (noteAnimation is TextureAnimation textureAnimation && textureAnimation.FrameCount > 0) + texture = textureAnimation.CurrentFrame; + + if (texture != null) + { + // The height is scaled to the minimum column width, if provided. + float minimumWidth = minimumColumnWidth ?? DrawWidth; + noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), texture.DisplayWidth); + } + } + + protected virtual void OnDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + directionContainer.Anchor = Anchor.TopCentre; + directionContainer.Scale = new Vector2(1, -1); + } + else + { + directionContainer.Anchor = Anchor.BottomCentre; + directionContainer.Scale = Vector2.One; + } + } + + protected virtual Drawable? GetAnimation(ISkinSource skin) => GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage); + + protected Drawable? GetAnimationFromLookup(ISkin skin, LegacyManiaSkinConfigurationLookups lookup) + { + string[] videoFiles = System.IO.Directory.GetFiles(@"Textures/Webm", "*.webm"); + + if (videoFiles.Length == 0) + { + // 没有找到视频文件,返回一个默认背景 + return new Background(getBackgroundTextureName()); + } + + string videoPath = videoFiles[RNG.Next(videoFiles.Length)]; + return new VideoBackgroundScreen(videoPath); + + return skin.GetAnimation(noteImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboText.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboText.cs deleted file mode 100644 index e40441989d..0000000000 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboText.cs +++ /dev/null @@ -1,61 +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 osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; -using osu.Game.Configuration; -using osu.Game.Localisation.SkinComponents; -using osu.Game.Resources.Localisation.Web; -using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD -{ - public partial class EzComComboText : ComboCounter - { - public EzComCounterText Text = null!; - - protected override double RollingDuration => 250; - - protected virtual bool DisplayXSymbol => true; - - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] - public Bindable ShowLabel { get; } = new BindableBool(true); - - [BackgroundDependencyLoader] - private void load(ScoreProcessor scoreProcessor) - { - Current.BindTo(scoreProcessor.Combo); - Current.BindValueChanged(combo => - { - bool wasIncrease = combo.NewValue > combo.OldValue; - bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0; - - float newScale = Math.Clamp(Text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f); - - float duration = wasMiss ? 1000 : 100; - - Text.NumberContainer - .ScaleTo(new Vector2(newScale)) - .ScaleTo(Vector2.One, duration, Easing.OutQuint); - - if (wasMiss) - Text.FlashColour(Color4.Red, duration, Easing.OutQuint); - }); - } - - protected override LocalisableString FormatCount(int count) => DisplayXSymbol ? @"Combo" : count.ToString(); - - protected override IHasText CreateText() => Text = new EzComCounterText(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper()) - { - ShowLabel = { BindTarget = ShowLabel }, - }; - } -} diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComCounterText.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComCounterText.cs index 86f7be2efc..5ee8b6cb7a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComCounterText.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComCounterText.cs @@ -57,7 +57,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD Anchor = Anchor.Centre, Origin = Anchor.BottomCentre, Text = label.GetValueOrDefault(), - Font = OsuFont.Stat.With(size: 12, weight: FontWeight.Bold), // Margin = new MarginPadding { Bottom = 1 }, }, textPart = new Ez2CounterSpriteText(textLookup) @@ -138,7 +137,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD private class GlyphStore : ITexturedGlyphLookupStore { - private readonly string fontName; + // private readonly string fontName; private readonly TextureStore textures; private readonly Func getLookup; @@ -146,17 +145,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD public GlyphStore(string fontName, TextureStore textures, Func getLookup) { - this.fontName = fontName; + // this.fontName = fontName; this.textures = textures; this.getLookup = getLookup; } public ITexturedCharacterGlyph? Get(string? fontName, char character) { - // We only service one font. - if (fontName != this.fontName) - return null; - if (cache.TryGetValue(character, out var cached)) return cached; diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTiming.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTiming.cs index a63a388fcd..bf0422dd75 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTiming.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComHitTiming.cs @@ -21,22 +21,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD private readonly Bindable hitResult = new Bindable(); [Resolved] - protected HitWindows HitWindows { get; private set; } + protected HitWindows HitWindows { get; private set; } = null!; [Resolved] - private ScoreProcessor processor { get; set; } + private ScoreProcessor processor { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; [Resolved(canBeNull: true)] - private GameplayClockContainer gameplayClockContainer { get; set; } + private GameplayClockContainer gameplayClockContainer { get; set; } = null!; [Resolved] - private DrawableRuleset drawableRuleset { get; set; } + private DrawableRuleset drawableRuleset { get; set; } = null!; - private OsuSpriteText offsetText; - private Box backgroundBox; + private OsuSpriteText offsetText = null!; + private Box backgroundBox = null!; public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComJudgementArea.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComJudgementArea.cs new file mode 100644 index 0000000000..1db1a58930 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComJudgementArea.cs @@ -0,0 +1,184 @@ +// 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.Graphics.Containers; +using osu.Game.Skinning; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Effects; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD +{ + public partial class EzComJudgementArea : CompositeDrawable, ISerialisableDrawable, IHitExplosion + { + public bool UsesFixedAnchor { get; set; } + + public override bool RemoveWhenNotAlive => true; + + private Container largeFaint = null!; + private Container background = null!; + private double bpm; + private readonly IBindable direction = new Bindable(); + + private Bindable accentColour = null!; + + [Resolved] + private Column column { get; set; } = null!; + + [Resolved] + private IBeatmap beatmap { get; set; } = null!; + + [Resolved] + private IGameplayClock gameplayClock { get; set; } = null!; + + [Resolved] + private StageDefinition stageDefinition { get; set; } = null!; + + public EzComJudgementArea() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + if (stageDefinition.Columns == 14 && column.Index == 13) + Alpha = 0; + + Size = new Vector2(2); + Alpha = 1; + + InternalChildren = new Drawable[] + { + background = new Container + { + Height = NOTE_HEIGHT, + Blending = BlendingParameters.Mixture, + Colour = Color4.Gray, + Alpha = 0.3f, + }, + largeFaint = new Container + { + Height = NOTE_HEIGHT, + Blending = BlendingParameters.Additive, + Alpha = 0.8f, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Masking = true, + Scale = new Vector2(0.5f), + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = new Color4(1f, 1f, 1f, 0.5f), + Radius = 2f, // 调整光晕半径 + Roundness = 0f, + }, + }, + }; + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + + accentColour = column.AccentColour.GetBoundCopy(); + accentColour.BindValueChanged(colour => + { + largeFaint.Colour = Interpolation.ValueAt(0.8f, colour.NewValue, Color4.White, 0, 1); + + largeFaint.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour.NewValue, + Roundness = Ez2NotePiece.NOTE_HEIGHT, + Radius = 50, + }; + }, true); + bpm = beatmap.ControlPointInfo.TimingPointAt(gameplayClock.CurrentTime).BPM * gameplayClock.GetTrueGameplayRate(); + } + + protected override void Update() + { + base.Update(); + Height = DrawWidth; + + if (stageDefinition.Columns == 14 && column.Index == 13) + Alpha = 0; + + double interval = 60000 / bpm; + const double amplitude = 6.0; + double progress = (gameplayClock.CurrentTime % interval) / interval; + + double smoothValue = smoothSineWave(progress); + Y = (float)(smoothValue * amplitude); + } + + private double smoothSineWave(double t) + { + const double frequency = 1; + const double amplitude = 0.3; + return amplitude * Math.Sin(frequency * t * 2 * Math.PI); + } + + private const float NOTE_HEIGHT = 40; + + public void Animate(JudgementResult result) + { + this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + Anchor = Origin = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; + } + + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action != column.Action.Value) return false; + + const double lighting_fade_in_duration = 70; + Color4 lightingColour = getLightingColour(); + + background.ScaleTo(0.9f, lighting_fade_in_duration, Easing.OutQuint); + background.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = lightingColour.Opacity(0.1f), + Radius = 20, + }, lighting_fade_in_duration, Easing.OutQuint); + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + if (e.Action != column.Action.Value) return; + + const double lighting_fade_out_duration = 800; + + Color4 lightingColour = getLightingColour().Opacity(0); + + background.ScaleTo(1f, 200, Easing.OutQuint); + background.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = lightingColour, + Radius = 20, + }, lighting_fade_out_duration, Easing.OutQuint); + } + + private Color4 getLightingColour() => Interpolation.ValueAt(0.2f, accentColour.Value, Color4.White, 0, 1); + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComJudgementTexture.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComJudgementTexture.cs new file mode 100644 index 0000000000..5c9f442d12 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComJudgementTexture.cs @@ -0,0 +1,226 @@ +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.Input.Events; +using osu.Game.Configuration; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD +{ + public partial class EzComJudgementTexture : CompositeDrawable, ISerialisableDrawable //, IAnimatableJudgement + { + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Playback FPS", "The FPS value of this Animation")] + public BindableNumber PlaybackFps { get; } = new BindableNumber(60) + { + MinValue = 1, + MaxValue = 240, + Precision = 1f, + }; + + private Vector2 dragStartPosition; + private bool isDragging; + + [Resolved] + private TextureStore textures { get; set; } = null!; + + protected HitWindows HitWindows { get; private set; } = null!; + + [Resolved] + private ScoreProcessor processor { get; set; } = null!; + + [Resolved(canBeNull: true)] + private GameplayClockContainer gameplayClockContainer { get; set; } = null!; + + public EzComJudgementTexture() + { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + // private (HitResult result, double length)[] hitWindows = null!; + + [BackgroundDependencyLoader] + private void load(DrawableRuleset drawableRuleset) + { + HitWindows = drawableRuleset.FirstAvailableHitWindows ?? HitWindows.Empty; + // hitWindows = HitWindows.GetAllAvailableWindows().ToArray(); + // This is to allow the visual state to be correct after HUD comes visible after being hidden. + AlwaysPresent = true; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + gameplayClockContainer.OnSeek += Clear; + + processor.NewJudgement += processorNewJudgement; + } + + private void processorNewJudgement(JudgementResult j) => Schedule(() => OnNewJudgement(j)); + + protected void OnNewJudgement(JudgementResult judgement) + { + if (!judgement.IsHit || judgement.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0) + return; + + if (!judgement.Type.IsScorable() || judgement.Type.IsBonus()) + return; + + ClearInternal(true); + + var judgementText = CreateJudgementTexture(judgement.Type); + AddInternal(judgementText); + } + + protected virtual void Clear() + { + FinishTransforms(true); + + ClearInternal(); + } + + protected override void Dispose(bool isDisposing) + { + PlaybackFps.UnbindAll(); + processor.NewJudgement -= processorNewJudgement; + gameplayClockContainer.OnSeek -= Clear; + base.Dispose(isDisposing); + } + + protected TextureAnimation CreateJudgementTexture(HitResult result) + { + var judgementTexture = new TextureAnimation + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + // DefaultFrameLength = 1000 / PlaybackFps.Value, + Loop = false + }; + + string gifPath = getGifPath(result); + + for (int i = 0;; i++) + { + var texture = textures.Get($@"{gifPath}/frame_{i}"); + if (texture == null) + break; + + judgementTexture.AddFrame(texture); + } + + PlaybackFps.BindValueChanged(fps => + { + judgementTexture.DefaultFrameLength = 1000 / fps.NewValue; + }, true); + + PlayAnimation(result, judgementTexture); + + judgementTexture.OnUpdate += _ => + { + if (judgementTexture.CurrentFrameIndex == judgementTexture.FrameCount - 1) + judgementTexture.Expire(); + }; + return judgementTexture; + } + + protected override bool OnDragStart(DragStartEvent e) + { + dragStartPosition = e.ScreenSpaceMousePosition; + isDragging = true; + return true; + } + + protected override void OnDrag(DragEvent e) + { + if (isDragging) + { + var delta = e.ScreenSpaceMousePosition - dragStartPosition; + Position += delta; + dragStartPosition = e.ScreenSpaceMousePosition; + } + } + + protected override void OnDragEnd(DragEndEvent e) + { + isDragging = false; + } + + private string getGifPath(HitResult hitResult) + { + return hitResult switch + { + HitResult.Miss => @"Gameplay/Ez2/Miss", + HitResult.Meh => @"Gameplay/Ez2/Fail1", + HitResult.Ok => @"Gameplay/Ez2/Fail", + HitResult.Good => @"Gameplay/Ez2/Good", + HitResult.Great => @"Gameplay/Ez2/Cool", + HitResult.Perfect => @"Gameplay/Ez2/Kool", + _ => @"Gameplay/Ez2", + }; + } + + public virtual void PlayAnimation(HitResult hitResult, Drawable drawable) + { + const float flash_speed = 60f; + + switch (hitResult) + { + case HitResult.Miss: + applyFadeEffect(drawable, new[] { Color4.Red, Color4.IndianRed }, flash_speed); + break; + + case HitResult.Meh: + applyFadeEffect(drawable, new[] { Color4.Purple, Color4.MediumPurple }, flash_speed); + break; + + case HitResult.Ok: + applyFadeEffect(drawable, new[] { Color4.ForestGreen, Color4.SeaGreen }, flash_speed); + break; + + case HitResult.Good: + applyFadeEffect(drawable, new[] { Color4.Green, Color4.LightGreen }, flash_speed); + break; + + case HitResult.Great: + applyFadeEffect(drawable, new[] { Color4.AliceBlue, Color4.LightSkyBlue }, flash_speed); + break; + + case HitResult.Perfect: + applyFadeEffect(drawable, new[] { Color4.LightBlue, Color4.LightGreen }, flash_speed); + break; + + default: + return; + } + } + + private void applyFadeEffect(Drawable drawable, Color4[] colors, double flashSpeed) + { + if (!drawable.IsLoaded) + return; + + drawable.FadeColour(colors[0], 0); + var sequence = drawable.FadeColour(colors[0], flashSpeed, Easing.OutQuint); + + for (int i = 1; i < colors.Length; i++) + { + sequence = sequence.Then().FadeColour(colors[i], flashSpeed, Easing.OutQuint); + } + + // sequence.Loop(); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/Ez2ScoreCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComScoreCounter.cs similarity index 79% rename from osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/Ez2ScoreCounter.cs rename to osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComScoreCounter.cs index b14e5b80c3..687a784ecf 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/Ez2ScoreCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComScoreCounter.cs @@ -14,20 +14,12 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD { - public partial class Ez2ScoreCounter : GameplayScoreCounter, ISerialisableDrawable + public partial class EzComScoreCounter : GameplayScoreCounter, ISerialisableDrawable { protected override double RollingDuration => 250; - [SettingSource("Wireframe opacity", "Controls the opacity of the wireframes behind the digits.")] - public BindableFloat WireframeOpacity { get; } = new BindableFloat(0) - { - Precision = 0.01f, - MinValue = 0, - MaxValue = 1, - }; - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] - public Bindable ShowLabel { get; } = new BindableBool(true); + public Bindable ShowLabel { get; } = new BindableBool(false); public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2JudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2JudgementPiece.cs index 6f945ce90f..2c0fe7f8ab 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2JudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2JudgementPiece.cs @@ -19,9 +19,9 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Ez2 { - public partial class Ez2JudgementPiece : TextJudgementPiece, IAnimatableJudgement + public partial class Ez2JudgementPiece : LAsJudgementText, IAnimatableJudgement { - private const float judgement_y_position = 140; + internal const float JUDGEMENT_Y_POSITION = 140; private RingExplosion? ringExplosion; @@ -33,7 +33,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 { AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; - Y = judgement_y_position; + Y = JUDGEMENT_Y_POSITION; + } + + public Ez2JudgementPiece() + { } [BackgroundDependencyLoader] @@ -78,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 JudgementText .ScaleTo(Vector2.One) .ScaleTo(new Vector2(1.3f), 1800, Easing.OutQuint); - this.MoveToY(judgement_y_position); + this.MoveToY(JUDGEMENT_Y_POSITION); applyFadeEffect(this, new[] { Color4.Red, Color4.IndianRed }, flash_speed); applyScaleAndFadeOutEffect(this, new Vector2(1.5f), 300, new Vector2(1.5f, 0.1f), 300, 300); @@ -88,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 JudgementText .ScaleTo(Vector2.One) .ScaleTo(new Vector2(1.3f), 1800, Easing.OutQuint); - this.MoveToY(judgement_y_position); + this.MoveToY(JUDGEMENT_Y_POSITION); applyFadeEffect(this, new[] { Color4.Purple, Color4.MediumPurple }, flash_speed); applyScaleAndFadeOutEffect(this, new Vector2(1.5f), 300, new Vector2(1.5f, 0.1f), 300, 300); @@ -98,14 +102,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 JudgementText .ScaleTo(Vector2.One) .ScaleTo(new Vector2(1.3f), 1800, Easing.OutQuint); - this.MoveToY(judgement_y_position); + this.MoveToY(JUDGEMENT_Y_POSITION); applyFadeEffect(this, new[] { Color4.ForestGreen, Color4.SeaGreen }, flash_speed); applyScaleAndFadeOutEffect(this, new Vector2(1.3f), 200, new Vector2(1.3f, 0.1f), 400, 400); break; case HitResult.Good: - this.MoveToY(judgement_y_position); + this.MoveToY(JUDGEMENT_Y_POSITION); JudgementText .ScaleTo(Vector2.One) .ScaleTo(new Vector2(1.3f), 1800, Easing.OutQuint); diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/GifJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/GifJudgementPiece.cs deleted file mode 100644 index f38b14e811..0000000000 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/GifJudgementPiece.cs +++ /dev/null @@ -1,158 +0,0 @@ -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; -using osu.Game.Skinning; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Mania.Skinning.Ez2 -{ - public partial class GifJudgementPiece : TextJudgementPiece, IAnimatableJudgement, ISerialisableDrawable - { - private TextureAnimation? gifAnimation; - private Vector2 dragStartPosition; - private bool isDragging; - public bool UsesFixedAnchor { get; set; } - - public GifJudgementPiece(HitResult result) - : base(result) - { - AutoSizeAxes = Axes.Both; - Origin = Anchor.Centre; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - if (Result.IsHit()) - { - gifAnimation = new TextureAnimation - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - DefaultFrameLength = 100, - // AutoSizeAxes = Axes.Both - }; - - string gifPath = getGifPath(Result); - - for (int i = 0; i < 26; i++) // 假设每个GIF有10帧 - { - gifAnimation.AddFrame(textures.Get($"{gifPath}/frame_{i}")); - } - - AddInternal(gifAnimation); - PlayAnimation(); - } - } - - protected override bool OnDragStart(DragStartEvent e) - { - dragStartPosition = e.ScreenSpaceMousePosition; - isDragging = true; - return true; - } - - protected override void OnDrag(DragEvent e) - { - if (isDragging) - { - var delta = e.ScreenSpaceMousePosition - dragStartPosition; - Position += delta; - dragStartPosition = e.ScreenSpaceMousePosition; - } - } - - protected override void OnDragEnd(DragEndEvent e) - { - isDragging = false; - } - - private string getGifPath(HitResult result) - { - return result switch - { - HitResult.Miss => "osu.Game.Resources/Skins/Ez2/Miss", - HitResult.Meh => "osu.Game.Resources/Skins/Ez2/Fail1", - HitResult.Ok => "osu.Game.Resources/Skins/Ez2/Fail", - HitResult.Good => "osu.Game.Resources/Skins/Ez2/Good", - HitResult.Great => "osu.Game.Resources/Skins/Ez2/Cool", - HitResult.Perfect => "osu.Game.Resources/Skins/Ez2/Kool", - _ => string.Empty, - }; - } - - protected override OsuSpriteText CreateJudgementText() => new OsuSpriteText(); - - public virtual void PlayAnimation() - { - const float flash_speed = 60f; - - switch (Result) - { - case HitResult.Miss: - applyFadeEffect(this, new[] { Color4.Red, Color4.IndianRed }, flash_speed); - break; - - case HitResult.Meh: - - applyFadeEffect(this, new[] { Color4.Purple, Color4.MediumPurple }, flash_speed); - break; - - case HitResult.Ok: - applyFadeEffect(this, new[] { Color4.ForestGreen, Color4.SeaGreen }, flash_speed); - break; - - case HitResult.Good: - applyFadeEffect(this, new[] { Color4.Green, Color4.LightGreen }, flash_speed); - break; - - case HitResult.Great: - applyFadeEffect(this, new[] { Color4.AliceBlue, Color4.LightSkyBlue }, flash_speed); - break; - - case HitResult.Perfect: - applyFadeEffect(this, new[] { Color4.LightBlue, Color4.LightGreen }, flash_speed); - break; - } - } - - private void applyFadeEffect(Drawable drawable, Color4[] colors, double flashSpeed) - { - var sequence = drawable.FadeColour(colors[0], flashSpeed, Easing.OutQuint); - - for (int i = 1; i < colors.Length; i++) - { - sequence = sequence.Then().FadeColour(colors[i], flashSpeed, Easing.OutQuint); - } - - sequence.Loop(); - } - - public Drawable? GetAboveHitObjectsProxiedContent() - { - return null; // 根据需要返回适当的 Drawable - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Schedule(() => Schedule(updateAnchor)); - } - - private void updateAnchor() - { - // 如果锚点不是垂直中心,则根据滚动方向设置顶部或底部锚点 - if (Anchor.HasFlag(Anchor.y1)) - return; - - Anchor &= ~(Anchor.y0 | Anchor.y2); - } - } -} diff --git a/osu.Game.Rulesets.Mania/Skinning/Ez2/ManiaEz2SkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Ez2/ManiaEz2SkinTransformer.cs index b9bcef5095..82c36f446f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Ez2/ManiaEz2SkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Ez2/ManiaEz2SkinTransformer.cs @@ -67,6 +67,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 hitTiming.Y = 500; } + var comboSprite = container.ChildrenOfType().FirstOrDefault(); + + if (comboSprite != null) + { + comboSprite.Anchor = Anchor.TopCentre; + comboSprite.Origin = Anchor.Centre; + comboSprite.Scale = new Vector2(0.8f); + comboSprite.Y = 190; + } + var combos = container.ChildrenOfType().ToArray(); if (combos.Length >= 2) @@ -78,7 +88,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 combo1.Origin = Anchor.Centre; combo1.Colour = Colour4.White; combo1.Y = 200; - combo1.ShowLabel.Value = true; + combo1.ShowLabel.Value = false; combo1.BoxAlpha.Value = 0.8f; combo1.IncreaseScale.Value = 1.5f; combo1.DecreaseScale.Value = 1f; @@ -88,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 combo2.Anchor = Anchor.TopCentre; combo2.Origin = Anchor.Centre; combo2.Colour = Colour4.White; - combo2.Y = 208; + combo2.Y = 200; combo2.ShowLabel.Value = false; combo2.BoxAlpha.Value = 0.2f; combo2.IncreaseScale.Value = 3f; @@ -121,15 +131,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 hitErrorMeter.CentreMarkerStyle.Value = BarHitErrorMeter.CentreMarkerStyles.Circle; hitErrorMeter.LabelStyle.Value = BarHitErrorMeter.LabelStyles.None; } + + var judgementPiece = container.OfType().FirstOrDefault(); + + if (judgementPiece != null) + { + judgementPiece.Anchor = Anchor.Centre; + judgementPiece.Origin = Anchor.Centre; + judgementPiece.Y = 50; + } }) { - // new EzComComboText(), + new EzComComboSprite(), new EzComComboCounter(), new EzComComboCounter(), new Ez2KeyCounterDisplay(), // new ArgonKeyCounterDisplay(), new BarHitErrorMeter(), // new EzComHitTiming(), + new EzComJudgementTexture(), }; } @@ -139,8 +159,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2 // if (Skin is Ez2Skin && resultComponent.Component > HitResult.Great) // return Drawable.Empty(); - return new Ez2JudgementPiece(resultComponent.Component); - // return new GifJudgementPiece(resultComponent.Component); + // return new Ez2JudgementPiece(resultComponent.Component); + return Drawable.Empty(); // return new DefaultSkinComponentsContainer(container => // { // }); diff --git a/osu.Game.Rulesets.Mania/Skinning/SbI/ManiaSbISkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/SbI/ManiaSbISkinTransformer.cs index ce70be968f..13c69bea89 100644 --- a/osu.Game.Rulesets.Mania/Skinning/SbI/ManiaSbISkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/SbI/ManiaSbISkinTransformer.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Skinning.Ez2; using osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD.HitErrorMeters; @@ -95,8 +94,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI case SkinComponentLookup resultComponent: // if (Skin is SbISkin && resultComponent.Component >= HitResult.Great) // return Drawable.Empty(); - return new GifJudgementPiece(resultComponent.Component); - // return new SbIJudgementPiece(resultComponent.Component); + // return new EzComJudgementTexture(resultComponent.Component); + return new SbIJudgementPiece(resultComponent.Component); case ManiaSkinComponentLookup maniaComponent: switch (maniaComponent.Component) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 77b16dd0c5..2e082c292b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -86,9 +86,12 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestSpinningSamplePitchShift() { + PausableSkinnableSound spinSample = null; + AddStep("Add spinner", () => SetContents(_ => testSingle(5, true, 4000))); - AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8); - AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8); + AddUntilStep("wait for spin sample", () => (spinSample = getSpinningSample()) != null); + AddUntilStep("Pitch starts low", () => spinSample.Frequency.Value < 0.8); + AddUntilStep("Pitch increases", () => spinSample.Frequency.Value > 0.8); PausableSkinnableSound getSpinningSample() => drawableSpinner.ChildrenOfType().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin")))); diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs index 1fbdbafec4..bb5499b1a5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs @@ -85,9 +85,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon { double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration); Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out)); + + // When hit, don't animate further. This avoids a scale being applied on a scale and looking very weird. + return; } - else - Scale = Vector2.One; + + Scale = Vector2.One; const float move_distance = -12; const float scale_amount = 1.3f; diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj index b02425eadd..a8fc9536b9 100644 --- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj +++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj @@ -28,6 +28,7 @@ + diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index da07373037..9f13b0587b 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -29,6 +29,7 @@ + diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 647c0aed75..b10cce6a52 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -135,6 +135,24 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestNoopFadeTransformIsIgnoredForLifetime() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("noop-fade-transform-is-ignored-for-lifetime.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + Assert.AreEqual(2, background.Elements.Count); + + Assert.AreEqual(1500, background.Elements[0].StartTime); + Assert.AreEqual(1500, background.Elements[1].StartTime); + } + } + [Test] public void TestOutOfOrderStartTimes() { @@ -288,6 +306,29 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestVideoWithCustomFadeIn() + { + var decoder = new LegacyStoryboardDecoder(); + + using var resStream = TestResources.OpenResource("video-custom-alpha-transform.osb"); + using var stream = new LineBufferedReader(resStream); + + var storyboard = decoder.Decode(stream); + + Assert.Multiple(() => + { + Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1)); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf()); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678)); + Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().StartTime, Is.EqualTo(1500)); + Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().EndTime, Is.EqualTo(1600)); + + Assert.That(storyboard.EarliestEventTime, Is.Null); + Assert.That(storyboard.LatestEventTime, Is.Null); + }); + } + [Test] public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds() { diff --git a/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs b/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs new file mode 100644 index 0000000000..4a80c71c3d --- /dev/null +++ b/osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs @@ -0,0 +1,73 @@ +// 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 Bogus; +using MessagePack; +using NUnit.Framework; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; + +namespace osu.Game.Tests.OnlinePlay +{ + [TestFixture] + public class MultiplayerPlaylistItemTest + { + [SetUp] + public void Setup() + { + Randomizer.Seed = new Random(1337); + } + + [Test] + public void TestCloneMultiplayerPlaylistItem() + { + var faker = new Faker() + .StrictMode(true) + .RuleFor(o => o.ID, f => f.Random.Long()) + .RuleFor(o => o.OwnerID, f => f.Random.Int()) + .RuleFor(o => o.BeatmapID, f => f.Random.Int()) + .RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash()) + .RuleFor(o => o.RulesetID, f => f.Random.Int()) + .RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) })) + .RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) })) + .RuleFor(o => o.Expired, f => f.Random.Bool()) + .RuleFor(o => o.PlaylistOrder, f => f.Random.UShort()) + .RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset()) + .RuleFor(o => o.StarRating, f => f.Random.Double()) + .RuleFor(o => o.Freestyle, f => f.Random.Bool()); + + for (int i = 0; i < 100; i++) + { + MultiplayerPlaylistItem item = faker.Generate(); + Assert.That(MessagePackSerializer.SerializeToJson(item.Clone()), Is.EqualTo(MessagePackSerializer.SerializeToJson(item))); + } + } + + [Test] + public void TestConstructFromAPIModel() + { + var faker = new Faker() + .StrictMode(true) + .RuleFor(o => o.ID, f => f.Random.Long()) + .RuleFor(o => o.OwnerID, f => f.Random.Int()) + .RuleFor(o => o.BeatmapID, f => f.Random.Int()) + .RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash()) + .RuleFor(o => o.RulesetID, f => f.Random.Int()) + .RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) })) + .RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) })) + .RuleFor(o => o.Expired, f => f.Random.Bool()) + .RuleFor(o => o.PlaylistOrder, f => f.Random.UShort()) + .RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset()) + .RuleFor(o => o.StarRating, f => f.Random.Double()) + .RuleFor(o => o.Freestyle, f => f.Random.Bool()); + + for (int i = 0; i < 100; i++) + { + MultiplayerPlaylistItem initialItem = faker.Generate(); + MultiplayerPlaylistItem copiedItem = new MultiplayerPlaylistItem(new PlaylistItem(initialItem)); + Assert.That(MessagePackSerializer.SerializeToJson(copiedItem), Is.EqualTo(MessagePackSerializer.SerializeToJson(initialItem))); + } + } + } +} diff --git a/osu.Game.Tests/Resources/noop-fade-transform-is-ignored-for-lifetime.osb b/osu.Game.Tests/Resources/noop-fade-transform-is-ignored-for-lifetime.osb new file mode 100644 index 0000000000..aca9bf926a --- /dev/null +++ b/osu.Game.Tests/Resources/noop-fade-transform-is-ignored-for-lifetime.osb @@ -0,0 +1,8 @@ +[Events] +//Storyboard Layer 0 (Background) +Sprite,Background,TopCentre,"img.jpg",320,240 + F,0,1000,1000,0,0 // should be ignored + F,0,1500,1600,0,1 +Sprite,Background,TopCentre,"img.jpg",320,240 + F,0,1000,1000,0,0 // should be ignored + F,0,1500,1600,1,1 diff --git a/osu.Game.Tests/Resources/video-custom-alpha-transform.osb b/osu.Game.Tests/Resources/video-custom-alpha-transform.osb new file mode 100644 index 0000000000..39fcf87c06 --- /dev/null +++ b/osu.Game.Tests/Resources/video-custom-alpha-transform.osb @@ -0,0 +1,5 @@ +osu file format v14 + +[Events] +Video,-5678,"Video.avi",0,0 + F,0,1500,1600,0,1 diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs index c974a852f3..185ebc1d39 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallenge.cs @@ -43,7 +43,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge { var room = new Room { - RoomID = 1234, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge { var room = new Room { - RoomID = 1234, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -99,7 +97,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge { var room = new Room { - RoomID = 1234, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -114,7 +111,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge }; AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); - AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); + AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value }); Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); @@ -128,7 +125,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge { var room = new Room { - RoomID = 1234, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -143,7 +139,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge }; AddStep("add room", () => API.Perform(new CreateRoomRequest(room))); - AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 }); + AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value }); Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!; AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room))); diff --git a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs index d6665e24a4..97b957df43 100644 --- a/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs +++ b/osu.Game.Tests/Visual/DailyChallenge/TestSceneDailyChallengeIntro.cs @@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.DailyChallenge [Test] public void TestDailyChallenge() { - startChallenge(1234); + startChallenge(); AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room))); } [Test] public void TestPlayIntroOnceFlag() { - startChallenge(1234); + startChallenge(); AddStep("set intro played flag", () => Dependencies.Get().SetValue(Static.DailyChallengeIntroPlayed, true)); - startChallenge(1235); + startChallenge(); AddAssert("intro played flag reset", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.False); @@ -62,13 +62,12 @@ namespace osu.Game.Tests.Visual.DailyChallenge AddUntilStep("intro played flag set", () => Dependencies.Get().Get(Static.DailyChallengeIntroPlayed), () => Is.True); } - private void startChallenge(int roomId) + private void startChallenge() { AddStep("add room", () => { API.Perform(new CreateRoomRequest(room = new Room { - RoomID = roomId, Name = "Daily Challenge: June 4, 2024", Playlist = [ @@ -83,7 +82,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge Category = RoomCategory.DailyChallenge })); }); - AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId })); + AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID!.Value })); } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs index 8b941d7597..092b2bc01c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs @@ -24,12 +24,7 @@ namespace osu.Game.Tests.Visual.Editing PoolableSkinnableSample[] loopingSamples = null; PoolableSkinnableSample[] onceOffSamples = null; - AddStep("get first slider", () => - { - slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); - onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray(); - loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray(); - }); + AddStep("get first slider", () => slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First()); AddStep("start playback", () => EditorClock.Start()); @@ -38,6 +33,9 @@ namespace osu.Game.Tests.Visual.Editing if (!slider.Tracking.Value) return false; + onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray(); + loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray(); + if (!loopingSamples.Any(s => s.Playing)) return false; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index aa99b22701..92a10628ff 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -50,21 +50,17 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("Set short reference score", () => { + // 50 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows List hitEvents = [ - // 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), - new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null), ]; + for (int i = 0; i < 49; i++) + { + hitEvents.Add(new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null)); + } + foreach (var ev in hitEvents) ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 91188f5bac..97889eea4d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings; using osu.Game.Overlays.SkinEditor; @@ -458,6 +459,62 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); } + [Test] + public void TestAnchorRadioButtonBehavior() + { + ISerialisableDrawable? selectedComponent = null; + + AddStep("Select first component", () => + { + var blueprint = skinEditor.ChildrenOfType().First(); + skinEditor.SelectedComponents.Clear(); + skinEditor.SelectedComponents.Add(blueprint.Item); + selectedComponent = blueprint.Item; + }); + + AddStep("Right-click to open context menu", () => + { + if (selectedComponent != null) + InputManager.MoveMouseTo(((Drawable)selectedComponent).ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Right); + }); + + AddStep("Click on Anchor menu", () => + { + InputManager.MoveMouseTo(getMenuItemByText("Anchor")); + InputManager.Click(MouseButton.Left); + }); + + AddStep("Right-click TopLeft anchor", () => + { + InputManager.MoveMouseTo(getMenuItemByText("TopLeft")); + InputManager.Click(MouseButton.Right); + }); + + AddAssert("TopLeft item checked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True); + + AddStep("Right-click Centre anchor", () => + { + InputManager.MoveMouseTo(getMenuItemByText("Centre")); + InputManager.Click(MouseButton.Right); + }); + + AddAssert("Centre item checked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True); + AddAssert("TopLeft item unchecked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False); + + AddStep("Right-click Closest anchor", () => + { + InputManager.MoveMouseTo(getMenuItemByText("Closest")); + InputManager.Click(MouseButton.Right); + }); + + AddAssert("Closest item checked", () => (getMenuItemByText("Closest").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True); + AddAssert("Centre item unchecked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False); + + Menu.DrawableMenuItem getMenuItemByText(string text) + => this.ChildrenOfType().First(m => m.Item.Text.ToString() == text); + } + private Skin importSkinFromArchives(string filename) { var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index e51ea12e83..14e6a67d3a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.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 System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -336,6 +337,61 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("button hidden", () => this.ChildrenOfType().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0)); } + [Test] + public void TestUserModSelectUpdatesWhenNotVisible() + { + AddStep("add playlist item", () => + { + room.Playlist = + [ + new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = [new APIMod(new OsuModFlashlight())] + } + ]; + }); + + ClickButtonWhenEnabled(); + AddUntilStep("wait for join", () => RoomJoined); + + // 1. Open the mod select overlay and enable flashlight + + ClickButtonWhenEnabled(); + AddUntilStep("mod select contents loaded", () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); + AddStep("click flashlight panel", () => + { + ModPanel panel = this.ChildrenOfType().Single(p => p.Mod is OsuModFlashlight); + InputManager.MoveMouseTo(panel); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("flashlight mod enabled", () => MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + + // 2. Close the mod select overlay, edit the playlist to disable allowed mods, and then edit it again to re-enable allowed mods. + + AddStep("close mod select overlay", () => this.ChildrenOfType().Single().Hide()); + AddUntilStep("mod select overlay not present", () => !this.ChildrenOfType().Single().IsPresent); + AddStep("disable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0]) + { + AllowedMods = [] + }))); + // This would normally be done as part of the above operation with an actual server. + AddStep("disable user mods", () => MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, Array.Empty())); + AddUntilStep("flashlight mod disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + AddStep("re-enable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0]) + { + AllowedMods = [new APIMod(new OsuModFlashlight())] + }))); + AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + + // 3. Open the mod select overlay, check that the flashlight mod panel is deactivated. + + ClickButtonWhenEnabled(); + AddUntilStep("mod select contents loaded", () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); + AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any()); + AddAssert("flashlight mod panel not activated", () => !this.ChildrenOfType().Single(p => p.Mod is OsuModFlashlight).Active.Value); + } + private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen { [Resolved(canBeNull: true)] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 1affa08813..c6a203c77a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Platform; @@ -32,7 +31,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps = null!; private BeatmapSetInfo importedSet = null!; private BeatmapInfo importedBeatmap = null!; - private Room room = null!; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -47,19 +45,17 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("create room", () => room = CreateDefaultRoom()); - AddStep("join room", () => JoinRoom(room)); + AddStep("join room", () => JoinRoom(CreateDefaultRoom())); WaitForJoined(); AddStep("create list", () => { - Child = list = new MultiplayerPlaylist(room) + Child = list = new MultiplayerPlaylist { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.4f, 0.8f), - SelectedItem = new Bindable() + Size = new Vector2(0.4f, 0.8f) }; }); @@ -158,37 +154,36 @@ namespace osu.Game.Tests.Visual.Multiplayer assertQueueTabCount(0); } - [Ignore("Expired items are initially removed from the room.")] [Test] public void TestJoinRoomWithMixedItemsAddedInCorrectLists() { AddStep("leave room", () => MultiplayerClient.LeaveRoom()); AddUntilStep("wait for room part", () => !RoomJoined); - AddStep("join room with items", () => + AddStep("join room with expired items", () => { - API.Queue(new CreateRoomRequest(new Room - { - Name = "test name", - Playlist = - [ - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID - }, - new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) - { - RulesetID = Ruleset.Value.OnlineID, - Expired = true - } - ] - })); + Room room = CreateDefaultRoom(); + room.Playlist = + [ + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID + }, + new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo) + { + RulesetID = Ruleset.Value.OnlineID, + Expired = true + } + ]; + + JoinRoom(room); }); - AddUntilStep("wait for room join", () => RoomJoined); + WaitForJoined(); - assertItemInQueueListStep(1, 0); - assertItemInHistoryListStep(2, 0); + // IDs are offset by 1 because we've joined two rooms in this test. + assertItemInQueueListStep(2, 0); + assertItemInHistoryListStep(3, 0); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs index 7283e3a1fe..d7659351bb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs @@ -49,13 +49,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create playlist", () => { - Child = playlist = new MultiplayerQueueList(room) + Child = playlist = new MultiplayerQueueList { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(500, 300), + Size = new Vector2(500, 300) }; + playlist.Items.ReplaceRange(0, playlist.Items.Count, MultiplayerClient.ClientAPIRoom!.Playlist); + MultiplayerClient.ClientAPIRoom!.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(Room.Playlist)) @@ -132,6 +134,18 @@ namespace osu.Game.Tests.Visual.Multiplayer assertDeleteButtonVisibility(1, false); } + [Test] + public void TestChangeExistingItem() + { + AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem + { + ID = playlist.Items[0].ID, + BeatmapID = 1337 + }).WaitSafely()); + + AddUntilStep("first playlist item has new beatmap", () => playlist.Items[0].Beatmap.OnlineID, () => Is.EqualTo(1337)); + } + private void addPlaylistItem(Func userId) { long itemId = -1; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index d622df8d76..9174b2a3db 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs @@ -9,6 +9,8 @@ using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Ranking; namespace osu.Game.Tests.Visual.Ranking @@ -20,10 +22,6 @@ namespace osu.Game.Tests.Visual.Ranking [SetUpSteps] public void SetUpSteps() { - AddStep("set up working beatmap", () => - { - Beatmap.Value.BeatmapInfo.OnlineID = 42; - }); AddStep("set up network requests", () => { dummyAPI.HandleRequest = request => @@ -40,6 +38,7 @@ namespace osu.Game.Tests.Visual.Ranking new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", }, new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", }, new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", }, + new APITag { Id = 5, Name = "mono-heavy", Description = "Features monos used in large amounts.", RulesetId = 1, }, ] }), 500); return true; @@ -67,19 +66,34 @@ namespace osu.Game.Tests.Visual.Ranking return false; }; }); - AddStep("create control", () => + AddStep("show for osu! beatmap", () => { - Child = new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - Child = new UserTagControl(Beatmap.Value.BeatmapInfo) - { - Width = 500, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }; + var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + working.BeatmapInfo.OnlineID = 42; + Beatmap.Value = working; + recreateControl(); }); + AddStep("show for taiko beatmap", () => + { + var working = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo); + working.BeatmapInfo.OnlineID = 44; + Beatmap.Value = working; + recreateControl(); + }); + } + + private void recreateControl() + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new UserTagControl(Beatmap.Value.BeatmapInfo) + { + Width = 500, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; } } } diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index a1f43505f0..c86f05c257 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,6 +1,7 @@  + diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs index 0a700eb4d6..1d91febd1a 100644 --- a/osu.Game.Tournament/Models/TournamentMatch.cs +++ b/osu.Game.Tournament/Models/TournamentMatch.cs @@ -19,6 +19,7 @@ namespace osu.Game.Tournament.Models { public int ID; + [JsonIgnore] public List Acronyms { get diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 765f2be345..e3ac0e1a3d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -36,6 +36,11 @@ namespace osu.Game.Beatmaps.Formats /// public const double CONTROL_POINT_LENIENCY = 5; + /// + /// The maximum allowed number of keys in mania beatmaps. + /// + public const int MAX_MANIA_KEY_COUNT = 18; + internal static RulesetStore? RulesetStore; private Beatmap beatmap = null!; @@ -116,7 +121,7 @@ namespace osu.Game.Beatmaps.Formats // mania uses "circle size" for key count, thus different allowable range difficulty.CircleSize = beatmap.BeatmapInfo.Ruleset.OnlineID != 3 ? Math.Clamp(difficulty.CircleSize, 0, 10) - : Math.Clamp(difficulty.CircleSize, 1, 18); + : Math.Clamp(difficulty.CircleSize, 1, MAX_MANIA_KEY_COUNT); difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10); difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10); diff --git a/osu.Game/Configuration/BackgroundSource.cs b/osu.Game/Configuration/BackgroundSource.cs index 2d9ed6df2c..237783ef99 100644 --- a/osu.Game/Configuration/BackgroundSource.cs +++ b/osu.Game/Configuration/BackgroundSource.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.ComponentModel; using osu.Framework.Localisation; using osu.Game.Localisation; @@ -16,5 +17,8 @@ namespace osu.Game.Configuration [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))] BeatmapWithStoryboard, + + [Description("Use 'Textures/Webm/*.webm' file from local folders")] + WebmSource, } } diff --git a/osu.Game/Configuration/SessionAverageHitErrorTracker.cs b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs index cd21eb6fa8..49f7657f91 100644 --- a/osu.Game/Configuration/SessionAverageHitErrorTracker.cs +++ b/osu.Game/Configuration/SessionAverageHitErrorTracker.cs @@ -40,10 +40,10 @@ namespace osu.Game.Configuration if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs)) return; - if (newScore.HitEvents.Count < 10) + if (newScore.HitEvents.Count < 50) return; - if (newScore.HitEvents.CalculateAverageHitError() is not double averageError) + if (newScore.HitEvents.CalculateMedianHitError() is not double medianError) return; // keep a sane maximum number of entries. @@ -51,7 +51,7 @@ namespace osu.Game.Configuration averageHitErrorHistory.RemoveAt(0); double globalOffset = configManager.Get(OsuSetting.AudioOffset); - averageHitErrorHistory.Add(new DataPoint(averageError, globalOffset)); + averageHitErrorHistory.Add(new DataPoint(medianError, globalOffset)); } public void ClearHistory() => averageHitErrorHistory.Clear(); diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index e538530b79..a3cdc2dc77 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -139,9 +139,14 @@ namespace osu.Game.Database notification.Progress = (float)current / tasks.Length; } } - catch (OperationCanceledException) + catch (OperationCanceledException cancelled) { - throw; + // We don't want to abort the full import process based off difficulty calculator's internal cancellation + // see https://github.com/ppy/osu/blob/91f3be5feaab0c73c17e1a8c270516aa9bee1e14/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs#L65. + if (cancelled.CancellationToken == notification.CancellationToken) + throw; + + Logger.Error(cancelled, $@"Timed out importing ({task})", LoggingTarget.Database); } catch (Exception e) { diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs index fc6d19ccf4..8a674dfa64 100644 --- a/osu.Game/Graphics/Containers/ScalingContainer.cs +++ b/osu.Game/Graphics/Containers/ScalingContainer.cs @@ -7,11 +7,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Layout; -using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Configuration; -using osu.Game.Localisation; using osu.Game.Rulesets; using osu.Game.Screens; using osu.Game.Screens.Backgrounds; @@ -211,9 +209,9 @@ namespace osu.Game.Graphics.Containers { if (targetMode == ScalingMode.Gameplay) { - if ((scalingGameMode.Value == ScalingGameMode.Std && ruleset.Value.OnlineID == 0) || + if ((scalingGameMode.Value == ScalingGameMode.Standard && ruleset.Value.OnlineID == 0) || (scalingGameMode.Value == ScalingGameMode.Taiko && ruleset.Value.OnlineID == 1) || - (scalingGameMode.Value == ScalingGameMode.Ctb && ruleset.Value.OnlineID == 2) || + (scalingGameMode.Value == ScalingGameMode.Catch && ruleset.Value.OnlineID == 2) || (scalingGameMode.Value == ScalingGameMode.Mania && ruleset.Value.OnlineID == 3)) { sizableContainer.RelativePositionAxes = Axes.Both; @@ -316,16 +314,12 @@ namespace osu.Game.Graphics.Containers public enum ScalingGameMode { - [LocalisableDescription(typeof(LayoutSettingsStrings), "Standard")] - Std, + Standard, - [LocalisableDescription(typeof(LayoutSettingsStrings), "Taiko")] Taiko, - [LocalisableDescription(typeof(LayoutSettingsStrings), "Mania")] Mania, - [LocalisableDescription(typeof(LayoutSettingsStrings), "CatchTheBeat")] - Ctb, + Catch, } } diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index d52ab466a4..7aa98ece95 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -28,8 +28,6 @@ namespace osu.Game.Graphics public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular); - public static FontUsage Stat => GetFont(Typeface.Stat, weight: FontWeight.Regular); - /// /// Retrieves a . /// @@ -72,9 +70,6 @@ namespace osu.Game.Graphics case Typeface.Inter: return @"Inter"; - - case Typeface.Stat: - return @"Stat"; } return null; @@ -121,7 +116,6 @@ namespace osu.Game.Graphics { Venera, Torus, - Stat, [Description("Torus (alternate)")] TorusAlternate, diff --git a/osu.Game/Localisation/BarHitErrorMeterStrings.cs b/osu.Game/Localisation/BarHitErrorMeterStrings.csss similarity index 100% rename from osu.Game/Localisation/BarHitErrorMeterStrings.cs rename to osu.Game/Localisation/BarHitErrorMeterStrings.csss diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 7162faa6e6..2715f0b8cf 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -109,21 +109,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ScoreDisplayMode => new TranslatableString(getKey(@"score_display_mode"), @"Score display mode"); - /// - /// "Hit Mode(No Active)" - /// - public static LocalisableString HitMode => new TranslatableString(getKey(@"hit_mode"), @"Hit Mode(No Active)"); - - /// - /// "Accuracy Cutoff S" - /// - public static LocalisableString AccuracyCutoffS => new TranslatableString(getKey(@"accuracy_cutoff_s"), @"Accuracy Cutoff S"); - - /// - /// "Accuracy Cutoff A" - /// - public static LocalisableString AccuracyCutoffA => new TranslatableString(getKey(@"accuracy_cutoff_a"), @"Accuracy Cutoff A"); - /// /// "Disable Windows key during gameplay" /// diff --git a/osu.Game/Localisation/HUD/BarHitErrorMeterStrings.cs b/osu.Game/Localisation/HUD/BarHitErrorMeterStrings.cs index ffc12b12de..2f77a287a0 100644 --- a/osu.Game/Localisation/HUD/BarHitErrorMeterStrings.cs +++ b/osu.Game/Localisation/HUD/BarHitErrorMeterStrings.cs @@ -9,8 +9,6 @@ namespace osu.Game.Localisation.HUD { private const string prefix = @"osu.Game.Resources.Localisation.HUD.BarHitErrorMeter"; - public static LocalisableString JudgementIconFadeOutDuration = new TranslatableString(getKey(@"Judgement_Icon_Fade_Out_Duration"), "Judgement Icon Fade Out Duration"); - /// /// "Judgement line thickness" /// diff --git a/osu.Game/Localisation/LayoutSettingsStrings.cs b/osu.Game/Localisation/LayoutSettingsStrings.cs index 8edf967f4c..a5172ec774 100644 --- a/osu.Game/Localisation/LayoutSettingsStrings.cs +++ b/osu.Game/Localisation/LayoutSettingsStrings.cs @@ -34,11 +34,6 @@ namespace osu.Game.Localisation /// public static LocalisableString ScaleEverythingExcludingOverlays => new TranslatableString(getKey(@"scale_everything_excluding_overlays"), @"Excluding overlays"); - public static LocalisableString Standard => "Standard"; - public static LocalisableString Taiko => "Taiko"; - public static LocalisableString Mania => "Mania"; - public static LocalisableString CatchTheBeat => "CatchTheBeat"; - /// /// "Everything" /// diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs index 80159b68f6..38912e0c85 100644 --- a/osu.Game/Localisation/RulesetSettingsStrings.cs +++ b/osu.Game/Localisation/RulesetSettingsStrings.cs @@ -82,7 +82,7 @@ namespace osu.Game.Localisation /// /// "{0}ms (speed {1:N1})" /// - public static LocalisableString ScrollSpeedTooltip(double scrollTime, double scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1:N1})", scrollTime, scrollSpeed); + public static LocalisableString ScrollSpeedTooltip(int scrollTime, double scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1:N1})", scrollTime, scrollSpeed); /// /// "Touch control scheme" diff --git a/osu.Game/Online/API/Requests/Responses/APITag.cs b/osu.Game/Online/API/Requests/Responses/APITag.cs index 4dd18663af..b0454fdb1d 100644 --- a/osu.Game/Online/API/Requests/Responses/APITag.cs +++ b/osu.Game/Online/API/Requests/Responses/APITag.cs @@ -15,5 +15,8 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("description")] public string Description { get; set; } = string.Empty; + + [JsonProperty("ruleset_id")] + public int? RulesetId { get; set; } } } diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs index 56f490cb21..49304c93a3 100644 --- a/osu.Game/Online/Chat/MessageNotifier.cs +++ b/osu.Game/Online/Chat/MessageNotifier.cs @@ -37,6 +37,9 @@ namespace osu.Game.Online.Chat [Resolved] private ChannelManager channelManager { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + [Resolved] private GameHost host { get; set; } @@ -47,19 +50,19 @@ namespace osu.Game.Online.Chat private readonly IBindableList joinedChannels = new BindableList(); [BackgroundDependencyLoader] - private void load(OsuConfigManager config, IAPIProvider api) + private void load(OsuConfigManager config) { notifyOnUsername = config.GetBindable(OsuSetting.NotifyOnUsernameMentioned); notifyOnPrivateMessage = config.GetBindable(OsuSetting.NotifyOnPrivateMessage); - - localUser.BindTo(api.LocalUser); - joinedChannels.BindTo(channelManager.JoinedChannels); } protected override void LoadComplete() { base.LoadComplete(); joinedChannels.BindCollectionChanged(channelsChanged, true); + + localUser.BindTo(api.LocalUser); + joinedChannels.BindTo(channelManager.JoinedChannels); } private void channelsChanged(object sender, NotifyCollectionChangedEventArgs e) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index db1722af8c..3c02565fa1 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -85,6 +85,7 @@ namespace osu.Game.Online.Multiplayer /// Retrieves the active as determined by the room's current settings. /// [IgnoreMember] + [JsonIgnore] public MultiplayerPlaylistItem CurrentPlaylistItem => Playlist.Single(item => item.ID == Settings.PlaylistItemId); /// diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 3234e28166..f58a67294e 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -62,11 +62,20 @@ namespace osu.Game.Online.Rooms [Key(11)] public bool Freestyle { get; set; } + /// + /// Creates a new . + /// [SerializationConstructor] public MultiplayerPlaylistItem() { } + /// + /// Creates a new from an API . + /// + /// + /// This will create unique instances of the and arrays but NOT unique instances of the contained s. + /// public MultiplayerPlaylistItem(PlaylistItem item) { ID = item.ID; @@ -82,5 +91,19 @@ namespace osu.Game.Online.Rooms StarRating = item.Beatmap.StarRating; Freestyle = item.Freestyle; } + + /// + /// Creates a copy of this . + /// + /// + /// This will create unique instances of the and arrays but NOT unique instances of the contained s. + /// + public MultiplayerPlaylistItem Clone() + { + MultiplayerPlaylistItem clone = (MultiplayerPlaylistItem)MemberwiseClone(); + clone.RequiredMods = RequiredMods.ToArray(); + clone.AllowedMods = AllowedMods.ToArray(); + return clone; + } } } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 817b42f503..427f31fc64 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -96,8 +96,14 @@ namespace osu.Game.Online.Rooms Beatmap = beatmap; } + /// + /// Creates a new from a . + /// + /// + /// This will create unique instances of the and arrays but NOT unique instances of the contained s. + /// public PlaylistItem(MultiplayerPlaylistItem item) - : this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating }) + : this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating, Checksum = item.BeatmapChecksum }) { ID = item.ID; OwnerID = item.OwnerID; diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs index 4f2dba7b2c..c2ffb8ba6c 100644 --- a/osu.Game/Overlays/OnScreenDisplay.cs +++ b/osu.Game/Overlays/OnScreenDisplay.cs @@ -5,8 +5,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; +using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; @@ -56,8 +59,11 @@ namespace osu.Game.Overlays /// The to be tracked. /// If is null. /// If is already being tracked from the same . - public void BeginTracking(object source, ITrackableConfigManager configManager) + /// An object representing the registration, that may be disposed to stop tracking the . + public IDisposable BeginTracking(object source, ITrackableConfigManager configManager) { + Debug.Assert(ThreadSafety.IsUpdateThread); + ArgumentNullException.ThrowIfNull(configManager); if (trackedConfigManagers.ContainsKey((source, configManager))) @@ -65,32 +71,18 @@ namespace osu.Game.Overlays var trackedSettings = configManager.CreateTrackedSettings(); if (trackedSettings == null) - return; + return new InvokeOnDisposal(() => { }); configManager.LoadInto(trackedSettings); trackedSettings.SettingChanged += displayTrackedSettingChange; - trackedConfigManagers.Add((source, configManager), trackedSettings); - } - /// - /// Unregisters a from having its settings tracked by this . - /// - /// The object that registered the to be tracked. - /// The that is being tracked. - /// If is null. - /// If is not being tracked from the same . - public void StopTracking(object source, ITrackableConfigManager configManager) - { - ArgumentNullException.ThrowIfNull(configManager); - - if (!trackedConfigManagers.TryGetValue((source, configManager), out var existing)) - return; - - existing.Unload(); - existing.SettingChanged -= displayTrackedSettingChange; - - trackedConfigManagers.Remove((source, configManager)); + return new InvokeOnDisposal(() => + { + trackedSettings.Unload(); + trackedSettings.SettingChanged -= displayTrackedSettingChange; + trackedConfigManagers.Remove((source, configManager)); + }); } /// diff --git a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index b93f996ec2..daf23c8ef3 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -62,8 +62,11 @@ namespace osu.Game.Overlays.Profile.Header.Components [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + [Resolved] + private IAPIProvider api { get; set; } = null!; + [BackgroundDependencyLoader] - private void load(IAPIProvider api, INotificationOverlay? notifications) + private void load(INotificationOverlay? notifications) { localUser.BindTo(api.LocalUser); @@ -73,15 +76,6 @@ namespace osu.Game.Overlays.Profile.Header.Components updateColor(); }); - User.BindValueChanged(u => - { - followerCount = u.NewValue?.User.FollowerCount ?? 0; - updateStatus(); - }, true); - - apiFriends.BindTo(api.Friends); - apiFriends.BindCollectionChanged((_, _) => Schedule(updateStatus)); - Action += () => { if (User.Value == null) @@ -126,6 +120,20 @@ namespace osu.Game.Overlays.Profile.Header.Components }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + apiFriends.BindTo(api.Friends); + apiFriends.BindCollectionChanged((_, _) => Schedule(updateStatus)); + + User.BindValueChanged(u => + { + followerCount = u.NewValue?.User.FollowerCount ?? 0; + updateStatus(); + }, true); + } + protected override bool OnHover(HoverEvent e) { if (status.Value > FriendStatus.None) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index dcaaf9752f..0e5e69b8c5 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -29,20 +29,20 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsEnumDropdown { ClassicDefault = HitWindows.HitMode.Ez2AcStyle, - LabelText = GameplaySettingsStrings.HitMode, + LabelText = "Hit Mode(No Active)", Current = config.GetBindable(OsuSetting.HitMode), Keywords = new[] { "scoring" } }, new SettingsSlider { - LabelText = GameplaySettingsStrings.AccuracyCutoffS, + LabelText = "Accuracy Cutoff S", Current = config.GetBindable(OsuSetting.AccuracyCutoffS), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { - LabelText = GameplaySettingsStrings.AccuracyCutoffA, + LabelText = "Accuracy Cutoff A", Current = config.GetBindable(OsuSetting.AccuracyCutoffA), KeyboardStep = 0.01f, DisplayAsPercentage = true diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 534abd1ab3..c1c64cac1f 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -760,11 +760,7 @@ namespace osu.Game.Overlays.SkinEditor #region Delegation of IEditorChangeHandler - public event Action? OnStateChange - { - add => throw new NotImplementedException(); - remove => throw new NotImplementedException(); - } + public event Action? OnStateChange; private IEditorChangeHandler? beginChangeHandler; @@ -773,6 +769,9 @@ namespace osu.Game.Overlays.SkinEditor // Change handler may change between begin and end, which can cause unbalanced operations. // Let's track the one that was used when beginning the change so we can call EndChange on it specifically. (beginChangeHandler = changeHandler)?.BeginChange(); + + if (beginChangeHandler != null) + beginChangeHandler.OnStateChange += OnStateChange; } public void EndChange() => beginChangeHandler?.EndChange(); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index bc878b9214..838b5ff2f0 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -22,7 +22,10 @@ namespace osu.Game.Overlays.SkinEditor { public partial class SkinSelectionHandler : SelectionHandler { - private OsuMenuItem originMenu = null!; + private OsuMenuItem? originMenu; + + private TernaryStateRadioMenuItem? closestAnchor; + private AnchorMenuItem[]? fixedAnchors; [Resolved] private SkinEditor skinEditor { get; set; } = null!; @@ -44,6 +47,38 @@ namespace osu.Game.Overlays.SkinEditor return scaleHandler; } + protected override void LoadComplete() + { + base.LoadComplete(); + + if (ChangeHandler != null) + ChangeHandler.OnStateChange += updateTernaryStates; + SelectedItems.BindCollectionChanged((_, _) => updateTernaryStates()); + } + + private void updateTernaryStates() + { + var usingClosestAnchor = GetStateFromSelection(SelectedBlueprints, c => !c.Item.UsesFixedAnchor); + + if (closestAnchor != null) + closestAnchor.State.Value = usingClosestAnchor; + + if (fixedAnchors != null) + { + foreach (var fixedAnchor in fixedAnchors) + fixedAnchor.State.Value = GetStateFromSelection(SelectedBlueprints, c => c.Item.UsesFixedAnchor && ((Drawable)c.Item).Anchor == fixedAnchor.Anchor); + } + + if (originMenu != null) + { + foreach (var origin in originMenu.Items.OfType()) + { + origin.State.Value = GetStateFromSelection(SelectedBlueprints, c => ((Drawable)c.Item).Origin == origin.Anchor); + origin.Action.Disabled = usingClosestAnchor == TernaryState.True; + } + } + } + public override bool HandleFlip(Direction direction, bool flipOverOrigin) { var selectionQuad = getSelectionQuad(); @@ -102,27 +137,17 @@ namespace osu.Game.Overlays.SkinEditor protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { - var closestItem = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors()) - { - State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) } - }; + closestAnchor = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors()); + fixedAnchors = createAnchorItems(applyFixedAnchors).ToArray(); yield return new OsuMenuItem(SkinEditorStrings.Anchor) { - Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors) - .Prepend(closestItem) - .ToArray() + Items = fixedAnchors.Prepend(closestAnchor).ToArray() }; yield return originMenu = new OsuMenuItem(SkinEditorStrings.Origin); - closestItem.State.BindValueChanged(s => - { - // For UX simplicity, origin should only be user-editable when "closest" anchor mode is disabled. - originMenu.Items = s.NewValue == TernaryState.True - ? Array.Empty() - : createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray(); - }, true); + originMenu.Items = createAnchorItems(applyOrigins).ToArray(); yield return new OsuMenuItemSpacer(); @@ -163,27 +188,37 @@ namespace osu.Game.Overlays.SkinEditor foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; - IEnumerable createAnchorItems(Func checkFunction, Action applyFunction) + updateTernaryStates(); + } + + private IEnumerable createAnchorItems(Action applyFunction) + { + var displayableAnchors = new[] { - var displayableAnchors = new[] - { - Anchor.TopLeft, - Anchor.TopCentre, - Anchor.TopRight, - Anchor.CentreLeft, - Anchor.Centre, - Anchor.CentreRight, - Anchor.BottomLeft, - Anchor.BottomCentre, - Anchor.BottomRight, - }; - return displayableAnchors.Select(a => - { - return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a)) - { - State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item, a)) } - }; - }); + Anchor.TopLeft, + Anchor.TopCentre, + Anchor.TopRight, + Anchor.CentreLeft, + Anchor.Centre, + Anchor.CentreRight, + Anchor.BottomLeft, + Anchor.BottomCentre, + Anchor.BottomRight, + }; + return displayableAnchors.Select(a => + { + return new AnchorMenuItem(a, _ => applyFunction(a)); + }); + } + + private partial class AnchorMenuItem : TernaryStateRadioMenuItem + { + public readonly Anchor Anchor; + + public AnchorMenuItem(Anchor anchor, Action applyFunction) + : base(anchor.ToString(), MenuItemType.Standard, _ => applyFunction(anchor)) + { + Anchor = anchor; } } diff --git a/osu.Game/Rulesets/Judgements/LAsJudgementText.cs b/osu.Game/Rulesets/Judgements/LAsJudgementText.cs new file mode 100644 index 0000000000..3749b6d674 --- /dev/null +++ b/osu.Game/Rulesets/Judgements/LAsJudgementText.cs @@ -0,0 +1,45 @@ +// 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.Extensions; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Judgements +{ + public abstract partial class LAsJudgementText : CompositeDrawable, ISerialisableDrawable + { + protected readonly HitResult Result; + + protected SpriteText JudgementText { get; private set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + public bool UsesFixedAnchor { get; set; } + + protected LAsJudgementText(HitResult result) + { + Result = result; + } + + protected LAsJudgementText() + { + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(JudgementText = CreateJudgementText()); + + JudgementText.Colour = colours.ForHitResult(Result); + JudgementText.Text = Result.GetDescription().ToUpperInvariant(); + } + + protected abstract SpriteText CreateJudgementText(); + } +} diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1f735576bc..265d4efac8 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; @@ -16,7 +15,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Lists; -using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; @@ -63,6 +61,8 @@ namespace osu.Game.Rulesets.Objects.Drawables protected PausableSkinnableSound Samples { get; private set; } + private bool samplesLoaded; + public virtual IEnumerable GetSamples() => HitObject.Samples; private readonly List nestedHitObjects = new List(); @@ -227,6 +227,12 @@ namespace osu.Game.Rulesets.Objects.Drawables comboColourBrightness.BindValueChanged(_ => UpdateComboColour()); + samplesBindable.BindCollectionChanged((_, _) => + { + if (samplesLoaded) + LoadSamples(); + }); + // Apply transforms updateStateFromResult(); } @@ -293,8 +299,6 @@ namespace osu.Game.Rulesets.Objects.Drawables } samplesBindable.BindTo(HitObject.SamplesBindable); - samplesBindable.BindCollectionChanged(onSamplesChanged, true); - HitObject.DefaultsApplied += onDefaultsApplied; OnApply(); @@ -335,11 +339,8 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.UnbindFrom(HitObject.SamplesBindable); - // When a new hitobject is applied, the samples will be cleared before re-populating. - // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). - samplesBindable.CollectionChanged -= onSamplesChanged; - // Release the samples for other hitobjects to use. + samplesLoaded = false; Samples?.ClearSamples(); foreach (var obj in nestedHitObjects) @@ -396,8 +397,6 @@ namespace osu.Game.Rulesets.Objects.Drawables Samples.Samples = samples.Cast().ToArray(); } - private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); - private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onRevertResult() @@ -631,6 +630,33 @@ namespace osu.Game.Rulesets.Objects.Drawables #endregion + protected override void Update() + { + // We use a flag here to load samples only when they are required to be played. + // Why in Update and not PlaySamples? Because some hit object implementations may expect LoadSamples to be called to load custom samples + // (slider slide sound as an example). + // + // This is best effort optimisation (over previous method of loading and de-pooling in `OnApply`) due to requiring knowledge of + // hitobjects' metadata. For cases like sliders with many repeats, there can be a sudden request to de-pool (ie slider with many repeats) + // hundreds of samples, causing a gameplay stutter. + // + // Note that we already have optimisations in OsuPlayfield for this but it applies to DrawableHitObjects and not samples. + // + // This is definitely not the end of optimisation of sample loading, but the structure of gameplay samples is going to take some + // time to dismantle and optimise. Optimally: + // + // - we would want to remove as much of the drawable overheads from samples as possible (currently two drawables per sample worst case) + // - pool the rawest representation of samples possible (if required at that point). + // - infer metadata at beatmap load to asynchronously preload the samples (into memory / bass). + if (!samplesLoaded) + { + samplesLoaded = true; + LoadSamples(); + } + + base.Update(); + } + public override bool UpdateSubTreeMasking() => false; protected override void UpdateAfterChildren() @@ -640,14 +666,6 @@ namespace osu.Game.Rulesets.Objects.Drawables UpdateResult(false); } - /// - /// Schedules an to this . - /// - /// - /// Only provided temporarily until hitobject pooling is implemented. - /// - protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action); - /// /// An offset prior to the start time of at which this may begin displaying contents. /// By default, s are assumed to display their contents within 10 seconds prior to the start time of . diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index fed0c3b51b..39fc8b357b 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -55,20 +55,23 @@ namespace osu.Game.Rulesets.Scoring } /// - /// Calculates the average hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. + /// Calculates the median hit offset/error for a sequence of s, where negative numbers mean the user hit too early on average. /// /// /// A non-null value if unstable rate could be calculated, /// and if unstable rate cannot be calculated due to being empty. /// - public static double? CalculateAverageHitError(this IEnumerable hitEvents) + public static double? CalculateMedianHitError(this IEnumerable hitEvents) { - double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); + double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).OrderBy(x => x).ToArray(); if (timeOffsets.Length == 0) return null; - return timeOffsets.Average(); + int center = timeOffsets.Length / 2; + + // Use average of the 2 central values if length is even + return timeOffsets.Length % 2 == 0 ? (timeOffsets[center - 1] + timeOffsets[center]) / 2 : timeOffsets[center]; } public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 13d4b67132..97c4ee45af 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -54,7 +54,12 @@ namespace osu.Game.Rulesets.UI /// /// The key conversion input manager for this DrawableRuleset. /// - protected PassThroughInputManager KeyBindingInputManager; + protected PassThroughInputManager KeyBindingInputManager { get; } + + /// + /// This configuration for this DrawableRuleset. + /// + protected IRulesetConfigManager Config { get; private set; } public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0; @@ -77,8 +82,26 @@ namespace osu.Game.Rulesets.UI public override IFrameStableClock FrameStableClock => frameStabilityContainer; + public override IEnumerable Objects => Beatmap.HitObjects; + + /// + /// The beatmap. + /// + [Cached(typeof(IBeatmap))] + public readonly Beatmap Beatmap; + + [Cached(typeof(IReadOnlyList))] + public sealed override IReadOnlyList Mods { get; } + + [Resolved(CanBeNull = true)] + private OnScreenDisplay onScreenDisplay { get; set; } + private readonly PlayfieldAdjustmentContainer playfieldAdjustmentContainer; + private IDisposable configTracker; + private FrameStabilityContainer frameStabilityContainer; + private DrawableRulesetDependencies dependencies; + private bool allowBackwardsSeeks; public override bool AllowBackwardsSeeks @@ -105,25 +128,6 @@ namespace osu.Game.Rulesets.UI } } - /// - /// The beatmap. - /// - [Cached(typeof(IBeatmap))] - public readonly Beatmap Beatmap; - - public override IEnumerable Objects => Beatmap.HitObjects; - - protected IRulesetConfigManager Config { get; private set; } - - [Cached(typeof(IReadOnlyList))] - public sealed override IReadOnlyList Mods { get; } - - private FrameStabilityContainer frameStabilityContainer; - - private OnScreenDisplay onScreenDisplay; - - private DrawableRulesetDependencies dependencies; - /// /// Creates a ruleset visualisation for the provided ruleset and beatmap. /// @@ -156,6 +160,9 @@ namespace osu.Game.Rulesets.UI { base.LoadComplete(); + if (Config != null) + configTracker = onScreenDisplay?.BeginTracking(this, Config); + IsPaused.ValueChanged += paused => { if (HasReplayLoaded.Value) @@ -168,13 +175,7 @@ namespace osu.Game.Rulesets.UI protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { dependencies = new DrawableRulesetDependencies(Ruleset, base.CreateChildDependencies(parent)); - Config = dependencies.RulesetConfigManager; - - onScreenDisplay = dependencies.Get(); - if (Config != null) - onScreenDisplay?.BeginTracking(this, Config); - return dependencies; } @@ -404,11 +405,7 @@ namespace osu.Game.Rulesets.UI { base.Dispose(isDisposing); - if (Config != null) - { - onScreenDisplay?.StopTracking(this, Config); - Config = null; - } + configTracker?.Dispose(); // Dispose the components created by this dependency container. dependencies?.Dispose(); diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 7be96718bd..dda6c70849 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -140,6 +140,23 @@ namespace osu.Game.Screens.Backgrounds private Background createBackground() { + switch (source.Value) + { + case BackgroundSource.WebmSource: + { + string[] videoFiles = System.IO.Directory.GetFiles(@"Textures/Webm", "*.webm"); + + if (videoFiles.Length == 0) + { + // 没有找到视频文件,返回一个默认背景 + return new Background(getBackgroundTextureName()); + } + + string videoPath = videoFiles[RNG.Next(videoFiles.Length)]; + return new VideoBackgroundScreen(videoPath); + } + } + // seasonal background loading gets highest priority. Background newBackground = seasonalBackgroundLoader.LoadNextBackground(); diff --git a/osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs b/osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs new file mode 100644 index 0000000000..e503eaefeb --- /dev/null +++ b/osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs @@ -0,0 +1,30 @@ +// VideoBackgroundScreen.cs + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Video; +using osu.Game.Graphics.Backgrounds; + +namespace osu.Game.Screens.Backgrounds +{ + public partial class VideoBackgroundScreen : Background + { + private readonly string videoPath; + + public VideoBackgroundScreen(string videoPath) + { + this.videoPath = videoPath; + } + + [BackgroundDependencyLoader] + private void load() + { + var video = new Video(videoPath) + { + RelativeSizeAxes = Axes.Both, + Loop = true + }; + AddInternal(video); + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index f3bd9ff257..f91a67a7e3 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -7,7 +7,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -217,7 +216,7 @@ namespace osu.Game.Screens.Edit.Timing bpmText = new OsuTextFlowContainer(st => { st.Font = OsuFont.Default.With(fixedWidth: true); - st.Spacing = new Vector2(-2.2f, 0); + st.Spacing = new Vector2(-1.9f, 0); }) { Name = @"BPM display", @@ -233,8 +232,7 @@ namespace osu.Game.Screens.Edit.Timing } private double effectiveBeatLength; - - private double effectiveBpm => 60_000 / effectiveBeatLength; + private double effectiveBpm; private TimingControlPoint timingPoint = null!; @@ -268,19 +266,26 @@ namespace osu.Game.Screens.Edit.Timing private void updateBpmText() { - int intPart = (int)interpolatedBpm.Value; + bool reachedFinalNumber = interpolatedBpm.Value == effectiveBpm; + int decimalPlaces = Math.Min(2, FormatUtils.FindPrecision((decimal)effectiveBpm)); - bpmText.Text = intPart.ToLocalisableString(); + string text = interpolatedBpm.Value.ToString($"N{decimalPlaces}"); + int? breakPoint = null; - // While interpolating between two integer values, showing the decimal places would look a bit odd - // so rounding is applied until we're close to the final value. - int decimalPlaces = FormatUtils.FindPrecision((decimal)effectiveBpm); - - if (decimalPlaces > 0) + for (int i = 0; i < text.Length; i++) { - bool reachedFinalNumber = intPart == (int)effectiveBpm; + if (!char.IsDigit(text[i])) + breakPoint = i; + } - bpmText.AddText((effectiveBpm % 1).ToLocalisableString("." + new string('0', decimalPlaces)), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.1f); + if (breakPoint != null) + { + bpmText.Text = text.Substring(0, breakPoint.Value); + bpmText.AddText(text.Substring(breakPoint.Value), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.2f); + } + else + { + bpmText.Text = text; } } @@ -300,6 +305,7 @@ namespace osu.Game.Screens.Edit.Timing if (effectiveBeatLength != timingPoint.BeatLength / Divisor) { effectiveBeatLength = timingPoint.BeatLength / Divisor; + effectiveBpm = TimingSection.BeatLengthToBpm(effectiveBeatLength); EarlyActivationMilliseconds = timingPoint.BeatLength / 2; diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index ae1ac02dd6..0c06a4e69b 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Edit.Timing try { if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0) - beatLengthBindable.Value = beatLengthToBpm(doubleVal); + beatLengthBindable.Value = BeatLengthToBpm(doubleVal); } catch { @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Edit.Timing beatLengthBindable.BindValueChanged(val => { - Current.Value = beatLengthToBpm(val.NewValue).ToString("N2"); + Current.Value = BeatLengthToBpm(val.NewValue).ToString("N2"); }, true); } @@ -146,6 +146,6 @@ namespace osu.Game.Screens.Edit.Timing } } - private static double beatLengthToBpm(double beatLength) => 60000 / beatLength; + public static double BeatLengthToBpm(double beatLength) => 60000 / beatLength; } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 57e8aff151..c73a36617d 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.ComponentModel; using System.Linq; using osu.Framework.Allocation; @@ -27,6 +26,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Utils; using Container = osu.Framework.Graphics.Containers.Container; @@ -62,11 +62,6 @@ namespace osu.Game.Screens.OnlinePlay.Match private Sample? sampleStart; - /// - /// Any mods applied by/to the local user. - /// - protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); - [Resolved(CanBeNull = true)] private IOverlayManager? overlayManager { get; set; } @@ -245,12 +240,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } }; - LoadComponent(UserModsSelectOverlay = new RoomModSelectOverlay - { - SelectedItem = { BindTarget = SelectedItem }, - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false - }); + LoadComponent(UserModsSelectOverlay = new MultiplayerUserModSelectOverlay()); } protected override void LoadComplete() @@ -258,7 +248,6 @@ namespace osu.Game.Screens.OnlinePlay.Match base.LoadComplete(); SelectedItem.BindValueChanged(_ => updateSpecifics()); - UserMods.BindValueChanged(_ => updateSpecifics()); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics()); @@ -441,11 +430,6 @@ namespace osu.Game.Screens.OnlinePlay.Match ? rulesetInstance.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray() : item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); - // Remove any user mods that are no longer allowed. - Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); - if (!newUserMods.SequenceEqual(UserMods.Value)) - UserMods.Value = newUserMods; - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info int beatmapId = GetGameplayBeatmap().OnlineID; var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId); @@ -456,15 +440,11 @@ namespace osu.Game.Screens.OnlinePlay.Match Ruleset.Value = GetGameplayRuleset(); if (allowedMods.Length > 0) - { UserModsSection.Show(); - UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); - } else { UserModsSection.Hide(); UserModsSelectOverlay.Hide(); - UserModsSelectOverlay.IsValidMod = _ => false; } if (item.Freestyle) @@ -488,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Match UserStyleSection.Hide(); } - protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray(); + protected virtual APIMod[] GetGameplayMods() => SelectedItem.Value!.RequiredMods; protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs new file mode 100644 index 0000000000..8463a4720c --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerUserModSelectOverlay.cs @@ -0,0 +1,124 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Threading; +using osu.Game.Configuration; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match +{ + public partial class MultiplayerUserModSelectOverlay : UserModSelectOverlay + { + [Resolved] + private MultiplayerClient client { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + private ModSettingChangeTracker? modSettingChangeTracker; + private ScheduledDelegate? debouncedModSettingsUpdate; + + public MultiplayerUserModSelectOverlay() + : base(OverlayColourScheme.Plum) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + client.RoomUpdated += onRoomUpdated; + SelectedMods.BindValueChanged(onSelectedModsChanged); + + updateValidMods(); + } + + private void onRoomUpdated() + { + // Importantly, this is not scheduled because the client must not skip intermediate server states to validate the allowed mods. + updateValidMods(); + } + + private void onSelectedModsChanged(ValueChangedEvent> mods) + { + modSettingChangeTracker?.Dispose(); + + if (client.Room == null) + return; + + client.ChangeUserMods(mods.NewValue).FireAndForget(); + + modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + modSettingChangeTracker.SettingChanged += _ => + { + // Debounce changes to mod settings so as to not thrash the network. + debouncedModSettingsUpdate?.Cancel(); + debouncedModSettingsUpdate = Scheduler.AddDelayed(() => + { + if (client.Room == null) + return; + + client.ChangeUserMods(SelectedMods.Value).FireAndForget(); + }, 500); + }; + } + + private void updateValidMods() + { + if (client.Room == null || client.LocalUser == null) + return; + + MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem; + Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance(); + Mod[] allowedMods = currentItem.Freestyle + ? ruleset.AllMods.OfType().Where(m => ModUtils.IsValidFreeModForMatchType(m, client.Room.Settings.MatchType)).ToArray() + : currentItem.AllowedMods.Select(m => m.ToMod(ruleset)).ToArray(); + + // Update the mod panels to reflect the ones which are valid for selection. + IsValidMod = allowedMods.Length > 0 + ? m => allowedMods.Any(a => a.GetType() == m.GetType()) + : _ => false; + + // Remove any mods that are no longer allowed. + Mod[] newUserMods = SelectedMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray(); + if (!newUserMods.SequenceEqual(SelectedMods.Value)) + SelectedMods.Value = newUserMods; + + // The active mods include the playlist item's required mods which change separately from the selected mods. + IReadOnlyList newActiveMods = ComputeActiveMods(); + if (!newActiveMods.SequenceEqual(ActiveMods.Value)) + ActiveMods.Value = newActiveMods; + } + + protected override IReadOnlyList ComputeActiveMods() + { + if (client.Room == null || client.LocalUser == null) + return []; + + MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem; + Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance(); + return currentItem.RequiredMods.Select(m => m.ToMod(ruleset)).Concat(base.ComputeActiveMods()).ToArray(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + client.RoomUpdated -= onRoomUpdated; + + modSettingChangeTracker?.Dispose(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs index 9feee0ae41..fba3acc32a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; @@ -19,12 +20,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist { public readonly Bindable DisplayMode = new Bindable(); - public required Bindable SelectedItem - { - get => selectedItem; - set => selectedItem.Current = value; - } - /// /// Invoked when an item requests to be edited. /// @@ -33,18 +28,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist [Resolved] private MultiplayerClient client { get; set; } = null!; - private readonly Room room; - private readonly BindableWithCurrent selectedItem = new BindableWithCurrent(); private MultiplayerPlaylistTabControl playlistTabControl = null!; private MultiplayerQueueList queueList = null!; private MultiplayerHistoryList historyList = null!; private bool firstPopulation = true; - public MultiplayerPlaylist(Room room) - { - this.room = room; - } - [BackgroundDependencyLoader] private void load() { @@ -65,17 +53,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist Masking = true, Children = new Drawable[] { - queueList = new MultiplayerQueueList(room) + queueList = new MultiplayerQueueList { RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = selectedItem }, RequestEdit = item => RequestEdit?.Invoke(item) }, historyList = new MultiplayerHistoryList { RelativeSizeAxes = Axes.Both, Alpha = 0, - SelectedItem = { BindTarget = selectedItem } } } } @@ -89,10 +75,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist base.LoadComplete(); DisplayMode.BindValueChanged(onDisplayModeChanged, true); + client.ItemAdded += playlistItemAdded; client.ItemRemoved += playlistItemRemoved; client.ItemChanged += playlistItemChanged; client.RoomUpdated += onRoomUpdated; + updateState(); } @@ -121,28 +109,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist firstPopulation = false; } + + PlaylistItem? currentItem = client.Room == null ? null : new PlaylistItem(client.Room.CurrentPlaylistItem); + queueList.SelectedItem.Value = currentItem; + historyList.SelectedItem.Value = currentItem; } - private void playlistItemAdded(MultiplayerPlaylistItem item) => Schedule(() => addItemToLists(item)); + private void playlistItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => addItemToLists(item)); - private void playlistItemRemoved(long item) => Schedule(() => removeItemFromLists(item)); + private void playlistItemRemoved(long item) => Scheduler.Add(() => removeItemFromLists(item)); - private void playlistItemChanged(MultiplayerPlaylistItem item) => Schedule(() => + private void playlistItemChanged(MultiplayerPlaylistItem item) => Scheduler.Add(() => { if (client.Room == null) return; - var newApiItem = new PlaylistItem(item); - var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID); + var existingItem = queueList.Items.SingleOrDefault(i => i.ID == item.ID); // Test if the only change between the two playlist items is the order. - if (existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem)) + if (existingItem != null && existingItem.With(playlistOrder: item.PlaylistOrder).Equals(new PlaylistItem(item))) { - // Set the new playlist order directly without refreshing the DrawablePlaylistItem. - existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder; - - // The following isn't really required, but is here for safety and explicitness. - // MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation. + // Set the new order directly and refresh the flow layout as an optimisation to avoid refreshing the items' visual state. + existingItem.PlaylistOrder = item.PlaylistOrder; queueList.Invalidate(); } else @@ -154,22 +142,35 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist private void addItemToLists(MultiplayerPlaylistItem item) { - var apiItem = client.Room?.Playlist.SingleOrDefault(i => i.ID == item.ID); - - // Item could have been removed from the playlist while the local player was in gameplay. - if (apiItem == null) + if (client.Room == null) return; if (item.Expired) - historyList.Items.Add(new PlaylistItem(apiItem)); + historyList.Items.Add(new PlaylistItem(item)); else - queueList.Items.Add(new PlaylistItem(apiItem)); + queueList.Items.Add(new PlaylistItem(item)); } - private void removeItemFromLists(long item) + private void removeItemFromLists(long itemId) { - queueList.Items.RemoveAll(i => i.ID == item); - historyList.Items.RemoveAll(i => i.ID == item); + if (client.Room == null) + return; + + queueList.Items.RemoveAll(i => i.ID == itemId); + historyList.Items.RemoveAll(i => i.ID == itemId); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client.IsNotNull()) + { + client.ItemAdded -= playlistItemAdded; + client.ItemRemoved -= playlistItemRemoved; + client.ItemChanged -= playlistItemChanged; + client.RoomUpdated -= onRoomUpdated; + } } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs index 04bb9b69e6..dc6a713908 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; @@ -20,50 +19,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist /// public partial class MultiplayerQueueList : DrawableRoomPlaylist { - private readonly Room room; - - private QueueFillFlowContainer flow = null!; - - public MultiplayerQueueList(Room room) + public MultiplayerQueueList() { - this.room = room; ShowItemOwners = true; } - protected override void LoadComplete() - { - base.LoadComplete(); - - room.PropertyChanged += onRoomPropertyChanged; - updateRoomPlaylist(); - } - - private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(Room.Playlist)) - updateRoomPlaylist(); - } - - private void updateRoomPlaylist() - => flow.InvalidateLayout(); - - protected override FillFlowContainer> CreateListFillFlowContainer() => flow = new QueueFillFlowContainer + protected override FillFlowContainer> CreateListFillFlowContainer() => new QueueFillFlowContainer { Spacing = new Vector2(0, 2) }; protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item); - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - room.PropertyChanged -= onRoomPropertyChanged; - } - private partial class QueueFillFlowContainer : FillFlowContainer> { - public new void InvalidateLayout() => base.InvalidateLayout(); - public override IEnumerable FlowingChildren => base.FlowingChildren.OfType>().OrderBy(item => item.Model.PlaylistOrder); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 6c6932f479..0cc033907f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.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.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -11,9 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Framework.Threading; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Online; using osu.Game.Online.API; @@ -23,7 +20,6 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -64,7 +60,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); - UserMods.BindValueChanged(onUserModsChanged); client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; @@ -145,11 +140,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer null, new Drawable[] { - new MultiplayerPlaylist(Room) + new MultiplayerPlaylist { RelativeSizeAxes = Axes.Both, - RequestEdit = OpenSongSelection, - SelectedItem = SelectedItem + RequestEdit = OpenSongSelection } }, new[] @@ -307,35 +301,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void PartRoom() => client.LeaveRoom(); - private ModSettingChangeTracker? modSettingChangeTracker; - private ScheduledDelegate? debouncedModSettingsUpdate; - - private void onUserModsChanged(ValueChangedEvent> mods) - { - modSettingChangeTracker?.Dispose(); - - if (client.Room == null) - return; - - client.ChangeUserMods(mods.NewValue).FireAndForget(); - - modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); - modSettingChangeTracker.SettingChanged += onModSettingsChanged; - } - - private void onModSettingsChanged(Mod mod) - { - // Debounce changes to mod settings so as to not thrash the network. - debouncedModSettingsUpdate?.Cancel(); - debouncedModSettingsUpdate = Scheduler.AddDelayed(() => - { - if (client.Room == null) - return; - - client.ChangeUserMods(UserMods.Value).FireAndForget(); - }, 500); - } - private void updateBeatmapAvailability(ValueChangedEvent availability) { if (client.Room == null) @@ -463,8 +428,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.RoomUpdated -= onRoomUpdated; client.LoadRequested -= onLoadRequested; } - - modSettingChangeTracker?.Dispose(); } public partial class AddItemButton : PurpleRoundedButton diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 1cb185dbab..f4a1cfa004 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters Precision = 0.1f, }; - [SettingSource(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.JudgementIconFadeOutDuration))] + [SettingSource("Icon Fade Out Duration", "Icon Fade Out Duration")] public BindableNumber JudgementFadeOutDuration { get; } = new BindableNumber(1200) { MinValue = 100, diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a738a40993..612d66a896 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -135,6 +135,8 @@ namespace osu.Game.Screens.Play public BreakOverlay BreakOverlay; + private LetterboxOverlay letterboxOverlay; + /// /// Whether the gameplay is currently in a break. /// @@ -277,6 +279,12 @@ namespace osu.Game.Screens.Play var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); GameplayClockContainer.Add(new GameplayScrollWheelHandling()); + // needs to exist in frame stable content, but is used by underlay layers so make sure assigned early. + breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Breaks = Beatmap.Value.Beatmap.Breaks + }; + // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. GameplayClockContainer.Add(rulesetSkinProvider); @@ -292,7 +300,7 @@ namespace osu.Game.Screens.Play Children = new[] { // underlay and gameplay should have access to the skinning sources. - createUnderlayComponents(), + createUnderlayComponents(Beatmap.Value), createGameplayComponents(Beatmap.Value) } }, @@ -335,10 +343,13 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(DrawableRuleset.FrameStableClock); dependencies.CacheAs(DrawableRuleset.FrameStableClock); + letterboxOverlay.Clock = DrawableRuleset.FrameStableClock; + letterboxOverlay.ProcessCustomClock = false; + // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. - failAnimationContainer.Add(createOverlayComponents(Beatmap.Value)); + failAnimationContainer.Add(createOverlayComponents()); if (!DrawableRuleset.AllowGameplayOverlays) { @@ -409,14 +420,22 @@ namespace osu.Game.Screens.Play protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); - private Drawable createUnderlayComponents() + private Drawable createUnderlayComponents(WorkingBeatmap working) { var container = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both }, + DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) + { + RelativeSizeAxes = Axes.Both + }, + letterboxOverlay = new LetterboxOverlay + { + BreakTracker = breakTracker, + Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, + }, new KiaiGameplayFountains(), }, }; @@ -434,15 +453,12 @@ namespace osu.Game.Screens.Play ScoreProcessor, HealthProcessor, new ComboEffects(ScoreProcessor), - breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Breaks = working.Beatmap.Breaks - } + breakTracker, }), } }; - private Drawable createOverlayComponents(IWorkingBeatmap working) + private Drawable createOverlayComponents() { var container = new Container { @@ -450,13 +466,6 @@ namespace osu.Game.Screens.Play Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), - new LetterboxOverlay - { - Clock = DrawableRuleset.FrameStableClock, - ProcessCustomClock = false, - BreakTracker = breakTracker, - Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0, - }, HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard) { HoldToQuit = diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index cef5884d39..23ccb3311b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -63,11 +63,11 @@ namespace osu.Game.Screens.Play.PlayerSettings [Resolved] private IGameplayClock? gameplayClock { get; set; } - private double lastPlayAverage; + private double lastPlayMedian; private double lastPlayBeatmapOffset; private HitEventTimingDistributionGraph? lastPlayGraph; - private SettingsButton? useAverageButton; + private SettingsButton? calibrateFromLastPlayButton; private IDisposable? beatmapOffsetSubscription; @@ -196,7 +196,7 @@ namespace osu.Game.Screens.Play.PlayerSettings var hitEvents = score.NewValue.HitEvents; - if (!(hitEvents.CalculateAverageHitError() is double average)) + if (!(hitEvents.CalculateMedianHitError() is double median)) return; referenceScoreContainer.Children = new Drawable[] @@ -210,7 +210,7 @@ namespace osu.Game.Screens.Play.PlayerSettings // affecting unstable rate here is used as a substitute of determining if a hit event represents a *timed* hit event, // i.e. an user input that the user had to *time to the track*, // i.e. one that it *makes sense to use* when doing anything with timing and offsets. - if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 10) + if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 50) { referenceScoreContainer.AddRange(new Drawable[] { @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.PlayerSettings return; } - lastPlayAverage = average; + lastPlayMedian = median; lastPlayBeatmapOffset = Current.Value; LinkFlowContainer globalOffsetText; @@ -239,7 +239,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Height = 50, }, new AverageHitError(hitEvents), - useAverageButton = new SettingsButton + calibrateFromLastPlayButton = new SettingsButton { Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay, Action = () => @@ -247,7 +247,7 @@ namespace osu.Game.Screens.Play.PlayerSettings if (Current.Disabled) return; - Current.Value = lastPlayBeatmapOffset - lastPlayAverage; + Current.Value = lastPlayBeatmapOffset - lastPlayMedian; lastAppliedScore.Value = ReferenceScore.Value; }, }, @@ -281,8 +281,8 @@ namespace osu.Game.Screens.Play.PlayerSettings bool allow = allowOffsetAdjust; - if (useAverageButton != null) - useAverageButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2); + if (calibrateFromLastPlayButton != null) + calibrateFromLastPlayButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayMedian, adjustmentSinceLastPlay, Current.Precision / 2); Current.Disabled = !allow; } diff --git a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs index fb7107cc88..29df085c62 100644 --- a/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs +++ b/osu.Game/Screens/Ranking/Statistics/AverageHitError.cs @@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Ranking.Statistics { /// - /// Displays the unstable rate statistic for a given play. + /// Displays the average hit error statistic for a given play. /// public partial class AverageHitError : SimpleStatisticItem { /// /// Creates and computes an statistic. /// - /// Sequence of s to calculate the unstable rate based on. + /// Sequence of s to calculate the average hit error based on. public AverageHitError(IEnumerable hitEvents) : base("Average Hit Error") { - Value = hitEvents.CalculateAverageHitError(); + Value = hitEvents.CalculateMedianHitError(); } protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}"; diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index ae4a918ae5..80d487112b 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -148,12 +148,14 @@ namespace osu.Game.Screens.Ranking if (allTags.Value == null || apiBeatmap.Value?.TopTags == null) return; - var allTagsById = allTags.Value.ToDictionary(t => t.Id); + var relevantTagsById = allTags.Value + .Where(tag => tag.RulesetId == null || tag.RulesetId == beatmapInfo.Ruleset.OnlineID) + .ToDictionary(t => t.Id); var ownTagIds = apiBeatmap.Value.OwnTagIds?.ToHashSet() ?? new HashSet(); foreach (var topTag in apiBeatmap.Value.TopTags) { - if (allTagsById.Remove(topTag.TagId, out var tag)) + if (relevantTagsById.Remove(topTag.TagId, out var tag)) { displayedTags.Add(new UserTag(tag) { @@ -163,7 +165,7 @@ namespace osu.Game.Screens.Ranking } } - extraTags.AddRange(allTagsById.Select(t => new UserTag(t.Value))); + extraTags.AddRange(relevantTagsById.Select(t => new UserTag(t.Value))); loadingLayer.Hide(); } diff --git a/osu.Game/Screens/Select/Filter/Operator.cs b/osu.Game/Screens/Select/Filter/Operator.cs index 706daf631f..e9d9af548e 100644 --- a/osu.Game/Screens/Select/Filter/Operator.cs +++ b/osu.Game/Screens/Select/Filter/Operator.cs @@ -11,6 +11,7 @@ namespace osu.Game.Screens.Select.Filter Less, LessOrEqual, Equal, + NotEqual, GreaterOrEqual, Greater } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 78f3bab114..1094d88730 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select public static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*""[!]?)|(\S*))", + @"\b(?\w+)(?(!?(:|=)|(>|<)(:|=)?))(?("".*""[!]?)|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) @@ -129,6 +129,10 @@ namespace osu.Game.Screens.Select case ":": return Operator.Equal; + case "!=": + case "!:": + return Operator.NotEqual; + case "<": return Operator.Less; diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index 42426c8c85..e10edfefe1 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -24,35 +24,48 @@ namespace osu.Game.Storyboards public readonly StoryboardCommandGroup Commands = new StoryboardCommandGroup(); - public double StartTime + public virtual double StartTime { get { - // To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero. - // A StartValue of zero governs, above all else, the first valid display time of a sprite. + // Users that are crafting storyboards using raw osb scripting or external tools may create alpha events far before the actual display time + // of sprites. // - // You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero, - // anything before that point can be ignored (the sprite is not visible after all). - var alphaCommands = new List<(double startTime, bool isZeroStartValue)>(); + // To make sure lifetime optimisations work as efficiently as they can, let's locally find the first time a sprite becomes visible. + var alphaCommands = new List>(); - var command = Commands.Alpha.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); + foreach (var command in Commands.Alpha) + { + alphaCommands.Add(command); + if (visibleAtStartOrEnd(command)) + break; + } foreach (var loop in loopingGroups) { - command = loop.Alpha.FirstOrDefault(); - if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0)); + foreach (var command in loop.Alpha) + { + alphaCommands.Add(command); + if (visibleAtStartOrEnd(command)) + break; + } } if (alphaCommands.Count > 0) { - var firstAlpha = alphaCommands.MinBy(t => t.startTime); + // Special care is given to cases where there's one or more no-op transforms (ie transforming from alpha 0 to alpha 0). + // - If a 0->0 transform exists, we still need to check it to ensure the absolute first start value is non-visible. + // - After ascertaining this, we then check the first non-noop transform to get the true start lifetime. + var firstAlpha = alphaCommands.MinBy(c => c.StartTime); + var firstRealAlpha = alphaCommands.Where(visibleAtStartOrEnd).MinBy(c => c.StartTime); - if (firstAlpha.isZeroStartValue) - return firstAlpha.startTime; + if (firstAlpha!.StartValue == 0 && firstRealAlpha != null) + return firstRealAlpha.StartTime; } return EarliestTransformTime; + + bool visibleAtStartOrEnd(StoryboardCommand command) => command.StartValue > 0 || command.EndValue > 0; } } diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 14189a1a6c..fb4ac56e98 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -14,9 +14,11 @@ namespace osu.Game.Storyboards { // This is just required to get a valid StartTime based on the incoming offset. // Actual fades are handled inside DrawableStoryboardVideo for now. - Commands.AddAlpha(Easing.None, offset, offset, 0, 0); + StartTime = offset; } + public override double StartTime { get; } + public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this); } } diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs index 46c1251d42..08f61f3ddc 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs @@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// The room host. public void AddServerSideRoom(Room room, APIUser host) { - room.RoomID ??= currentRoomId++; + room.RoomID = currentRoomId++; room.Host = host; for (int i = 0; i < room.Playlist.Count; i++)