From c3df7e1fa8c897971e9662f909ada3444e203930 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 01:05:12 +0900 Subject: [PATCH 01/23] Fix scroll container's scrollbar not respecting minimum size on first resize --- osu.Game/Graphics/Containers/OsuScrollContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index d504a11b22..ed5c73bee6 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -112,6 +112,9 @@ namespace osu.Game.Graphics.Containers CornerRadius = 5; + // needs to be set initially for the ResizeTo to respect minimum size + Size = new Vector2(SCROLL_BAR_HEIGHT); + const float margin = 3; Margin = new MarginPadding From acfa62bb50e669cdcfdc45f24f67339df7799eba Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 19:25:58 +0900 Subject: [PATCH 02/23] Fix potential taiko crash on rewind --- osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs | 13 +++++++++---- .../DrawableTestStrongHit.cs | 15 +++------------ .../Skinning/TestSceneHitExplosion.cs | 10 ++++------ osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs | 12 +++++++++--- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 11 +++++------ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 8 ++++---- 6 files changed, 34 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index 1db07b3244..3ffc6187b7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -2,26 +2,31 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Tests { - internal class DrawableTestHit : DrawableTaikoHitObject + public class DrawableTestHit : DrawableHit { - private readonly HitResult type; + public readonly HitResult Type; public DrawableTestHit(Hit hit, HitResult type = HitResult.Great) : base(hit) { - this.type = type; + Type = type; + + // in order to create nested strong hit + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } [BackgroundDependencyLoader] private void load() { - Result.Type = type; + Result.Type = Type; } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs index 7cb984b254..829bcf34a1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs @@ -2,17 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Tests { - public class DrawableTestStrongHit : DrawableHit + public class DrawableTestStrongHit : DrawableTestHit { - private readonly HitResult type; private readonly bool hitBoth; public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true) @@ -20,12 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { IsStrong = true, StartTime = startTime, - }) + }, type) { - // in order to create nested strong hit - HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - this.type = type; this.hitBoth = hitBoth; } @@ -33,10 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { base.LoadAsyncComplete(); - Result.Type = type; - var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single(); - nestedStrongHit.Result.Type = hitBoth ? type : HitResult.Miss; + nestedStrongHit.Result.Type = hitBoth ? Type : HitResult.Miss; } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index 48969e0f5a..19cc56527e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; namespace osu.Game.Rulesets.Taiko.Tests.Skinning @@ -29,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth)))); } - private Drawable getContentFor(DrawableTaikoHitObject hit) + private Drawable getContentFor(DrawableTestHit hit) { return new Container { @@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning Children = new Drawable[] { hit, - new HitExplosion(hit) + new HitExplosion(hit, hit.Type) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -46,9 +45,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }; } - private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); + private DrawableTestHit createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); - private DrawableTaikoHitObject createStrongHit(HitResult type, bool hitBoth) - => new DrawableTestStrongHit(Time.Current, type, hitBoth); + private DrawableTestHit createStrongHit(HitResult type, bool hitBoth) => new DrawableTestStrongHit(Time.Current, type, hitBoth); } } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs index 9943a58e3e..7b8ab89233 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs @@ -15,8 +15,14 @@ namespace osu.Game.Rulesets.Taiko.UI { internal class DefaultHitExplosion : CircularContainer { - [Resolved] - private DrawableHitObject judgedObject { get; set; } + private readonly DrawableHitObject judgedObject; + private readonly HitResult result; + + public DefaultHitExplosion(DrawableHitObject judgedObject, HitResult result) + { + this.judgedObject = judgedObject; + this.result = result; + } [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -31,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI Alpha = 0.15f; Masking = true; - if (judgedObject.Result.Type == HitResult.Miss) + if (result == HitResult.Miss) return; bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index e3eabbf88f..16300d5715 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -22,8 +22,8 @@ namespace osu.Game.Rulesets.Taiko.UI { public override bool RemoveWhenNotAlive => true; - [Cached(typeof(DrawableHitObject))] public readonly DrawableHitObject JudgedObject; + private readonly HitResult result; private SkinnableDrawable skinnable; @@ -31,9 +31,10 @@ namespace osu.Game.Rulesets.Taiko.UI public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; - public HitExplosion(DrawableHitObject judgedObject) + public HitExplosion(DrawableHitObject judgedObject, HitResult result) { JudgedObject = judgedObject; + this.result = result; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -47,14 +48,12 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion()); + Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion(JudgedObject, result)); } private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject) { - var resultType = judgedObject.Result?.Type ?? HitResult.Great; - - switch (resultType) + switch (result) { case HitResult.Miss: return TaikoSkinComponents.TaikoExplosionMiss; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 7976d5bc6d..7b3fbb1faf 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Taiko.Objects.Drawables; @@ -206,8 +207,7 @@ namespace osu.Game.Rulesets.Taiko.UI }); var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; - - addExplosion(judgedObject, type); + addExplosion(judgedObject, result.Type, type); break; } } @@ -219,9 +219,9 @@ namespace osu.Game.Rulesets.Taiko.UI /// As legacy skins have different explosions for singular and double strong hits, /// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame. /// - private void addExplosion(DrawableHitObject drawableObject, HitType type) => Schedule(() => + private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) => Schedule(() => { - hitExplosionContainer.Add(new HitExplosion(drawableObject)); + hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); }); From 480eeb5fbee83ce1ac7eb5c28fb55e71b78c4640 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 19:37:34 +0900 Subject: [PATCH 03/23] Add back caching --- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index 16300d5715..efd1b25046 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -22,7 +22,9 @@ namespace osu.Game.Rulesets.Taiko.UI { public override bool RemoveWhenNotAlive => true; + [Cached(typeof(DrawableHitObject))] public readonly DrawableHitObject JudgedObject; + private readonly HitResult result; private SkinnableDrawable skinnable; From 0853f0e128dc14a880bc8d7ae0c2553fd67b4b9f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 19:38:23 +0900 Subject: [PATCH 04/23] Remove comment --- osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index 3ffc6187b7..e0af973b53 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Tests { Type = type; - // in order to create nested strong hit HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } From 9a24346a008ed8c730bea94adb9271324c5a79a7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 23:29:40 +0900 Subject: [PATCH 05/23] Fix HP drain edgecase potentially causing insta-fails --- .../TestSceneDrainingHealthProcessor.cs | 39 +++++++++++++++++++ .../Scoring/DrainingHealthProcessor.cs | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 460ad1b898..4876d051aa 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -1,15 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Visual; @@ -175,6 +178,24 @@ namespace osu.Game.Tests.Gameplay assertHealthNotEqualTo(0); } + [Test] + public void TestSingleLongObjectDoesNotDrain() + { + var beatmap = new Beatmap + { + HitObjects = { new JudgeableLongHitObject() } + }; + + beatmap.HitObjects[0].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + createProcessor(beatmap); + setTime(0); + assertHealthEqualTo(1); + + setTime(5000); + assertHealthEqualTo(1); + } + private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) { var beatmap = new Beatmap @@ -235,5 +256,23 @@ namespace osu.Game.Tests.Gameplay } } } + + private class JudgeableLongHitObject : JudgeableHitObject, IHasDuration + { + public double EndTime => StartTime + Duration; + public double Duration { get; set; } = 5000; + + public JudgeableLongHitObject() + : base(false) + { + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + base.CreateNestedHitObjects(cancellationToken); + + AddNested(new JudgeableHitObject()); + } + } } } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 130907b242..ba9bbb055f 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Scoring private double computeDrainRate() { - if (healthIncreases.Count == 0) + if (healthIncreases.Count <= 1) return 0; int adjustment = 1; From b64e69fabd46f83e64853e8630ac06090032f904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 17:18:50 +0200 Subject: [PATCH 06/23] Add test hits to playfields directly where possible --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 47d8a5c012..36289bda93 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning foreach (var playfield in playfields) { var hit = new DrawableTestHit(new Hit(), judgementResult.Type); - Add(hit); + playfield.Add(hit); playfield.OnNewResult(hit, judgementResult); } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 99d1b72ea4..9fc07340c6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; - Add(h); + drawableRuleset.Playfield.Add(h); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); } @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; - Add(h); + drawableRuleset.Playfield.Add(h); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addMissJudgement() { DrawableTestHit h; - Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); + drawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); } From 095686a320823d80be303f2d6028a0da1bf4bb8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 17:26:26 +0200 Subject: [PATCH 07/23] Hide test hit directly in explosion scene --- osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs | 6 ++++++ .../Skinning/TestSceneHitExplosion.cs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index e0af973b53..4eeb4a1475 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -22,6 +22,12 @@ namespace osu.Game.Rulesets.Taiko.Tests HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } + protected override void UpdateInitialTransforms() + { + // base implementation in DrawableHitObject forces alpha to 1. + // suppress locally to allow hiding the visuals wherever necessary. + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index 19cc56527e..45c94a8a86 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -35,7 +35,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - hit, + // the hit needs to be added to hierarchy in order for nested objects to be created correctly. + // setting zero alpha is supposed to prevent the test from looking broken. + hit.With(h => h.Alpha = 0), new HitExplosion(hit, hit.Type) { Anchor = Anchor.Centre, From b1e02db8742e0510d88c68cbe0e15823abee86e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 20:36:38 +0200 Subject: [PATCH 08/23] Extract base taiko drawable ruleset scene --- .../DrawableTaikoRulesetTestScene.cs | 55 ++++++++++++++ .../TestSceneHits.cs | 74 +++++-------------- 2 files changed, 75 insertions(+), 54 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs new file mode 100644 index 0000000000..d1c4a1c56d --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public abstract class DrawableTaikoRulesetTestScene : OsuTestScene + { + protected DrawableTaikoRuleset DrawableRuleset { get; private set; } + protected Container PlayfieldContainer { get; private set; } + + [BackgroundDependencyLoader] + private void load() + { + var controlPointInfo = new ControlPointInfo(); + controlPointInfo.Add(0, new TimingControlPoint()); + + WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap + { + HitObjects = new List { new Hit { Type = HitType.Centre } }, + BeatmapInfo = new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + Metadata = new BeatmapMetadata + { + Artist = @"Unknown", + Title = @"Sample Beatmap", + AuthorString = @"peppy", + }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + ControlPointInfo = controlPointInfo + }); + + Add(PlayfieldContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 768, + Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) } + }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 9fc07340c6..b6cfe368f7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -2,11 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -18,13 +16,12 @@ using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; -using osu.Game.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] - public class TestSceneHits : OsuTestScene + public class TestSceneHits : DrawableTaikoRulesetTestScene { private const double default_duration = 3000; private const float scroll_time = 1000; @@ -32,8 +29,6 @@ namespace osu.Game.Rulesets.Taiko.Tests protected override double TimePerAction => default_duration * 2; private readonly Random rng = new Random(1337); - private DrawableTaikoRuleset drawableRuleset; - private Container playfieldContainer; [BackgroundDependencyLoader] private void load() @@ -64,35 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Tests AddStep("Height test 4", () => changePlayfieldSize(4)); AddStep("Height test 5", () => changePlayfieldSize(5)); AddStep("Reset height", () => changePlayfieldSize(6)); - - var controlPointInfo = new ControlPointInfo(); - controlPointInfo.Add(0, new TimingControlPoint()); - - WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap - { - HitObjects = new List { new Hit { Type = HitType.Centre } }, - BeatmapInfo = new BeatmapInfo - { - BaseDifficulty = new BeatmapDifficulty(), - Metadata = new BeatmapMetadata - { - Artist = @"Unknown", - Title = @"Sample Beatmap", - AuthorString = @"peppy", - }, - Ruleset = new TaikoRuleset().RulesetInfo - }, - ControlPointInfo = controlPointInfo - }); - - Add(playfieldContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = 768, - Children = new[] { drawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) } - }); } private void changePlayfieldSize(int step) @@ -128,11 +94,11 @@ namespace osu.Game.Rulesets.Taiko.Tests switch (step) { default: - playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500); + PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500); break; case 6: - playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); + PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); break; } } @@ -149,9 +115,9 @@ namespace osu.Game.Rulesets.Taiko.Tests var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; - drawableRuleset.Playfield.Add(h); + DrawableRuleset.Playfield.Add(h); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); } private void addStrongHitJudgement(bool kiai) @@ -166,37 +132,37 @@ namespace osu.Game.Rulesets.Taiko.Tests var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; - drawableRuleset.Playfield.Add(h); + DrawableRuleset.Playfield.Add(h); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); } private void addMissJudgement() { DrawableTestHit h; - drawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); + DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); } private void addBarLine(bool major, double delay = scroll_time) { - BarLine bl = new BarLine { StartTime = drawableRuleset.Playfield.Time.Current + delay }; + BarLine bl = new BarLine { StartTime = DrawableRuleset.Playfield.Time.Current + delay }; - drawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); + DrawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); } private void addSwell(double duration = default_duration) { var swell = new Swell { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, Duration = duration, }; swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableSwell(swell)); + DrawableRuleset.Playfield.Add(new DrawableSwell(swell)); } private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false) @@ -206,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var d = new DrumRoll { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong, Duration = duration, TickRate = 8, @@ -217,33 +183,33 @@ namespace osu.Game.Rulesets.Taiko.Tests d.ApplyDefaults(cpi, new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableDrumRoll(d)); + DrawableRuleset.Playfield.Add(new DrawableDrumRoll(d)); } private void addCentreHit(bool strong) { Hit h = new Hit { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong }; h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableHit(h)); + DrawableRuleset.Playfield.Add(new DrawableHit(h)); } private void addRimHit(bool strong) { Hit h = new Hit { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong }; h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableHit(h)); + DrawableRuleset.Playfield.Add(new DrawableHit(h)); } private class TestStrongNestedHit : DrawableStrongNestedHit From 0563a488f479083260753908b2ca8c140d3d98b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 20:37:23 +0200 Subject: [PATCH 09/23] Add failing test case --- .../TestSceneFlyingHits.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs new file mode 100644 index 0000000000..7492a76a67 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TestSceneFlyingHits : DrawableTaikoRulesetTestScene + { + [TestCase(HitType.Centre)] + [TestCase(HitType.Rim)] + public void TestFlyingHits(HitType hitType) + { + DrawableFlyingHit flyingHit = null; + + AddStep("add flying hit", () => + { + addFlyingHit(hitType); + + // flying hits all land in one common scrolling container (and stay there for rewind purposes), + // so we need to manually get the latest one. + flyingHit = this.ChildrenOfType() + .OrderByDescending(h => h.HitObject.StartTime) + .FirstOrDefault(); + }); + + AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType); + } + + private void addFlyingHit(HitType hitType) + { + var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; + + DrawableDrumRollTick h; + DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(tick, new TaikoDrumRollTickJudgement()) { Type = HitResult.Perfect }); + } + } +} From d61a8327da94243b65d7761ee23d9a5f4649a5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 20:59:55 +0200 Subject: [PATCH 10/23] Fix rim flying hits changing colour --- .../Objects/Drawables/DrawableFlyingHit.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index 460e760629..3253c1ce5a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -27,5 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables base.LoadComplete(); ApplyResult(r => r.Type = r.Judgement.MaxResult); } + + protected override void LoadSamples() + { + // block base call - flying hits are not supposed to play samples + // the base call could overwrite the type of this hit + } } } From 00aea7748970cb3d29bde9f7a171d6ba5a545480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Sep 2020 11:18:13 +0200 Subject: [PATCH 11/23] Fix potential instability of overlay activation tests --- .../Visual/Gameplay/TestSceneOverlayActivation.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 3ee0f4e720..e36cc6861d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -12,6 +12,14 @@ namespace osu.Game.Tests.Visual.Gameplay { protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer; + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddUntilStep("gameplay has started", + () => Player.GameplayClockContainer.GameplayClock.CurrentTime > Player.DrawableRuleset.GameplayStartTime); + } + [Test] public void TestGameplayOverlayActivation() { From 8d9945dea895ab03f7fcb9562a1d533ebc502d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Sep 2020 11:28:20 +0200 Subject: [PATCH 12/23] Change until step to assert for consistency --- osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index e36cc6861d..ce04b940e7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestGameplayOverlayActivationPaused() { - AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("pause gameplay", () => Player.Pause()); AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); } From 3cf430f49434ed3883ca1c5dc06844ca3d287327 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 15:30:18 +0900 Subject: [PATCH 13/23] Avoid saving state changes if nothing has changed (via binary comparison) --- .../Editing/EditorChangeHandlerTest.cs | 57 ++++++++++++++++++- osu.Game/Screens/Edit/EditorChangeHandler.cs | 28 +++++---- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index feda1ae0e9..ff2c9fb1a9 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Tests.Editing @@ -13,11 +15,12 @@ namespace osu.Game.Tests.Editing [Test] public void TestSaveRestoreState() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var (handler, beatmap) = createChangeHandler(); Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False); + addArbitraryChange(beatmap); handler.SaveState(); Assert.That(handler.CanUndo.Value, Is.True); @@ -29,15 +32,48 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanRedo.Value, Is.True); } + [Test] + public void TestSaveSameStateDoesNotSave() + { + var (handler, beatmap) = createChangeHandler(); + + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.False); + + addArbitraryChange(beatmap); + handler.SaveState(); + + Assert.That(handler.CanUndo.Value, Is.True); + Assert.That(handler.CanRedo.Value, Is.False); + + string hash = handler.CurrentStateHash; + + // save a save without making any changes + handler.SaveState(); + + Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); + + handler.RestoreState(-1); + + Assert.That(hash, Is.Not.EqualTo(handler.CurrentStateHash)); + + // we should only be able to restore once even though we saved twice. + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.True); + } + [Test] public void TestMaxStatesSaved() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var (handler, beatmap) = createChangeHandler(); Assert.That(handler.CanUndo.Value, Is.False); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) + { + addArbitraryChange(beatmap); handler.SaveState(); + } Assert.That(handler.CanUndo.Value, Is.True); @@ -53,12 +89,15 @@ namespace osu.Game.Tests.Editing [Test] public void TestMaxStatesExceeded() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var (handler, beatmap) = createChangeHandler(); Assert.That(handler.CanUndo.Value, Is.False); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++) + { + addArbitraryChange(beatmap); handler.SaveState(); + } Assert.That(handler.CanUndo.Value, Is.True); @@ -70,5 +109,17 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanUndo.Value, Is.False); } + + private (EditorChangeHandler, EditorBeatmap) createChangeHandler() + { + var beatmap = new EditorBeatmap(new Beatmap()); + + return (new EditorChangeHandler(beatmap), beatmap); + } + + private void addArbitraryChange(EditorBeatmap beatmap) + { + beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) }); + } } } diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index aa0f89912a..286fdbb020 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Logging; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; @@ -89,23 +91,27 @@ namespace osu.Game.Screens.Edit if (isRestoring) return; - if (currentState < savedStates.Count - 1) - savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); - - if (savedStates.Count > MAX_SAVED_STATES) - savedStates.RemoveAt(0); - using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw); - savedStates.Add(stream.ToArray()); + var newState = stream.ToArray(); + + // if the previous state is binary equal we don't need to push a new one, unless this is the initial state. + if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return; + + if (currentState < savedStates.Count - 1) + savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); + + if (savedStates.Count > MAX_SAVED_STATES) + savedStates.RemoveAt(0); + + savedStates.Add(newState); + + currentState = savedStates.Count - 1; + updateBindables(); } - - currentState = savedStates.Count - 1; - - updateBindables(); } /// From 1aa8b400d432b17031d4d5a37898dc0a3ac66d2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:45:36 +0900 Subject: [PATCH 14/23] Avoid unnecessary object updates from SelectionHandlers --- osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs | 8 +++++++- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index a3ecf7ed95..d5dd758e10 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -57,7 +57,13 @@ namespace osu.Game.Rulesets.Taiko.Edit ChangeHandler.BeginChange(); foreach (var h in hits) - h.IsStrong = state; + { + if (h.IsStrong != state) + { + h.IsStrong = state; + EditorBeatmap.UpdateHitObject(h); + } + } ChangeHandler.EndChange(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 6ca85fe026..a0220cf987 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -288,8 +288,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { var comboInfo = h as IHasComboInformation; - if (comboInfo == null) - continue; + if (comboInfo == null || comboInfo.NewCombo == state) continue; comboInfo.NewCombo = state; EditorBeatmap?.UpdateHitObject(h); From 0ae2266b8229ba5c8192385230c4506b2bf3e5a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:51:28 +0900 Subject: [PATCH 15/23] Fix new placement hitobjects in the editor not getting the default sample added --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 81d7fa4b32..9b3314e2ad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -212,6 +212,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if (blueprint != null) { + // doing this post-creations as adding the default hit sample should be the case regardless of the ruleset. + blueprint.HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL }); + placementBlueprintContainer.Child = currentPlacement = blueprint; // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame From a4e9c85333b06d293cc88a921ac38612490b1c04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:37:55 +0900 Subject: [PATCH 16/23] Trigger a hitobject update after blueprint drag ends --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 8908520cd7..970e16d1c3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -201,6 +201,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (isDraggingBlueprint) { + // handle positional change etc. + foreach (var obj in selectedHitObjects) + Beatmap.UpdateHitObject(obj); + changeHandler?.EndChange(); isDraggingBlueprint = false; } From 6095446f10df0a34b1676aa1bf46deb9cceb7a26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:15:26 +0900 Subject: [PATCH 17/23] Fix autoplay generators failing on empty hitobjects lists --- osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs | 3 +++ osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs | 3 +++ osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 3 +++ osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 5d11c574b1..a4f54bfe82 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Catch.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + // todo: add support for HT DT const double dash_speed = Catcher.BASE_SPEED; const double movement_speed = dash_speed / 2; diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 483327d5b3..3ebbe5af8e 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Mania.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); var actions = new List(); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 76b2631894..9b350278f3 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Osu.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + buttonIndex = 0; AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500))); diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 273f4e4105..db2e5948f5 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Taiko.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + bool hitButton = true; Frames.Add(new TaikoReplayFrame(-100000)); From e8220cf1b62dd7fcdd2afa13bb39b53b8a3771a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:03:56 +0900 Subject: [PATCH 18/23] Allow attaching a replay to a FrameStabilityContainer when FrameStablePlayback is off --- .../Rulesets/UI/FrameStabilityContainer.cs | 72 ++++++++++++------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index d574991fa0..a4af92749f 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -123,39 +123,63 @@ namespace osu.Game.Rulesets.UI try { - if (!FrameStablePlayback) - return; - - if (firstConsumption) + if (FrameStablePlayback) { - // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. - // Instead we perform an initial seek to the proposed time. + if (firstConsumption) + { + // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. + // Instead we perform an initial seek to the proposed time. - // process frame (in addition to finally clause) to clear out ElapsedTime - manualClock.CurrentTime = newProposedTime; - framedClock.ProcessFrame(); + // process frame (in addition to finally clause) to clear out ElapsedTime + manualClock.CurrentTime = newProposedTime; + framedClock.ProcessFrame(); - firstConsumption = false; - } - else if (manualClock.CurrentTime < gameplayStartTime) - manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime); - else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) - { - newProposedTime = newProposedTime > manualClock.CurrentTime - ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time) - : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time); + firstConsumption = false; + } + else if (manualClock.CurrentTime < gameplayStartTime) + manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime); + else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) + { + newProposedTime = newProposedTime > manualClock.CurrentTime + ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time) + : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time); + } } if (isAttached) { - double? newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime); + double? newTime; - if (newTime == null) + if (FrameStablePlayback) { - // we shouldn't execute for this time value. probably waiting on more replay data. - validState = false; - requireMoreUpdateLoops = true; - return; + // when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy. + if ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) == null) + { + // setting invalid state here ensures that gameplay will not continue (ie. our child + // hierarchy won't be updated). + validState = false; + + // potentially loop to catch-up playback. + requireMoreUpdateLoops = true; + + return; + } + } + else + { + // when stability is disabled, we don't really care about accuracy. + // looping over the replay will allow it to catch up and feed out the required values + // for the current time. + while ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) != newProposedTime) + { + if (newTime == null) + { + // special case for when the replay actually can't arrive at the required time. + // protects from potential endless loop. + validState = false; + return; + } + } } newProposedTime = newTime.Value; From ff7c904996083e985dd41b389656b190f357b202 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:03:37 +0900 Subject: [PATCH 19/23] Add autoplay mod in editor specific ruleset construction --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b9b7c1ef54..6e377ff207 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Edit try { - drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap)) + drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap, new[] { Ruleset.GetAutoplayMod() })) { Clock = EditorClock, ProcessCustomClock = false From 524c2b678c68b71d604342b32e5274fbb7684607 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:15:54 +0900 Subject: [PATCH 20/23] Forcefully regenerate autoplay on editor changes --- .../Rulesets/Edit/DrawableEditRulesetWrapper.cs | 8 ++++++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 89e7866707..1070b8cbd2 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -45,15 +45,21 @@ namespace osu.Game.Rulesets.Edit base.LoadComplete(); beatmap.HitObjectAdded += addHitObject; + beatmap.HitObjectUpdated += updateReplay; beatmap.HitObjectRemoved += removeHitObject; } + private void updateReplay(HitObject obj = null) => + drawableRuleset.RegenerateAutoplay(); + private void addHitObject(HitObject hitObject) { var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject); drawableRuleset.Playfield.Add(drawableObject); drawableRuleset.Playfield.PostProcess(); + + updateReplay(); } private void removeHitObject(HitObject hitObject) @@ -62,6 +68,8 @@ namespace osu.Game.Rulesets.Edit drawableRuleset.Playfield.Remove(drawableObject); drawableRuleset.Playfield.PostProcess(); + + drawableRuleset.RegenerateAutoplay(); } public override bool PropagatePositionalInputSubTree => false; diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index fbb9acfe90..50e9a93e22 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -151,8 +151,11 @@ namespace osu.Game.Rulesets.UI public virtual PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new PlayfieldAdjustmentContainer(); + [Resolved] + private OsuConfigManager config { get; set; } + [BackgroundDependencyLoader] - private void load(OsuConfigManager config, CancellationToken? cancellationToken) + private void load(CancellationToken? cancellationToken) { InternalChildren = new Drawable[] { @@ -178,11 +181,18 @@ namespace osu.Game.Rulesets.UI .WithChild(ResumeOverlay))); } - applyRulesetMods(Mods, config); + RegenerateAutoplay(); loadObjects(cancellationToken); } + public void RegenerateAutoplay() + { + // for now this is applying mods which aren't just autoplay. + // we'll need to reconsider this flow in the future. + applyRulesetMods(Mods, config); + } + /// /// Creates and adds drawable representations of hit objects to the play field. /// From 7949eabaac45fe2a67ab5a4f283c8851e314300c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 15:49:45 +0900 Subject: [PATCH 21/23] Remove left-over using --- osu.Game/Screens/Edit/EditorChangeHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 286fdbb020..617c436ee0 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Text; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Logging; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; From 467a16bf750600b0edc47674acd022334c632d89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 16:21:20 +0900 Subject: [PATCH 22/23] Fix fade out extension logic (and make it generally look better for sliders) --- .../Edit/DrawableOsuEditRuleset.cs | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index a8719e0aa8..01e59c9598 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. /// - private const double editor_hit_object_fade_out_extension = 500; + private const double editor_hit_object_fade_out_extension = 700; public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) @@ -32,20 +33,37 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState(DrawableHitObject hitObject, ArmedState state) { - switch (state) + if (state == ArmedState.Idle) + return; + + // adjust the visuals of certain object types to make them stay on screen for longer than usual. + switch (hitObject) { - case ArmedState.Miss: - // Get the existing fade out transform - var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); - if (existing == null) - return; - - hitObject.RemoveTransform(existing); - - using (hitObject.BeginAbsoluteSequence(existing.StartTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + case DrawableSlider slider: + // no specifics to sliders but let them fade slower below. break; + + case DrawableHitCircle circle: // also handles slider heads + circle.ApproachCircle + .FadeOutFromOne(editor_hit_object_fade_out_extension) + .Expire(); + break; + + default: + // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) + return; } + + // Get the existing fade out transform + var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); + + if (existing == null) + return; + + hitObject.RemoveTransform(existing); + + using (hitObject.BeginAbsoluteSequence(existing.StartTime)) + hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); } protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor(); From 5237fa7bf24cdaca9e2a8c2e24bdceff5906ff84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 16:37:54 +0900 Subject: [PATCH 23/23] Remove unused local in case statement --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 01e59c9598..746ff4ac19 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -39,7 +39,11 @@ namespace osu.Game.Rulesets.Osu.Edit // adjust the visuals of certain object types to make them stay on screen for longer than usual. switch (hitObject) { - case DrawableSlider slider: + default: + // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) + return; + + case DrawableSlider _: // no specifics to sliders but let them fade slower below. break; @@ -48,10 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit .FadeOutFromOne(editor_hit_object_fade_out_extension) .Expire(); break; - - default: - // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) - return; } // Get the existing fade out transform