diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs index 36c34a8fb9..a162c5ec44 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs @@ -15,7 +15,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Mania.Tests.Editor @@ -35,7 +34,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestPlaceBeforeCurrentTimeDownwards() { - AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10))); + AddStep("move mouse before current time", () => + { + var column = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100)); + }); AddStep("click", () => InputManager.Click(MouseButton.Left)); @@ -45,7 +48,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestPlaceAfterCurrentTimeDownwards() { - AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("move mouse after current time", () => + { + var column = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100)); + }); AddStep("click", () => InputManager.Click(MouseButton.Left)); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs index bde323f187..ca323b5911 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground()) + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) { RelativeSizeAxes = Axes.Both } @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground()) + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs index 7e80419944..c58c07c83b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 0), _ => new DefaultKeyArea()) + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) { RelativeSizeAxes = Axes.Both }, @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 1), _ => new DefaultKeyArea()) + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) { RelativeSizeAxes = Axes.Both }, diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs new file mode 100644 index 0000000000..4a6c59e297 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs @@ -0,0 +1,67 @@ +// 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.Framework.Graphics; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneDrawableManiaHitObject : OsuTestScene + { + private readonly ManualClock clock = new ManualClock(); + + private Column column; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new ScrollingTestContainer(ScrollingDirection.Down) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + TimeRange = 2000, + Clock = new FramedClock(clock), + Child = column = new Column(0) + { + Action = { Value = ManiaAction.Key1 }, + Height = 0.85f, + AccentColour = Color4.Gray + }, + }; + }); + + [Test] + public void TestHoldNoteHeadVisibility() + { + DrawableHoldNote note = null; + AddStep("Add hold note", () => + { + var h = new HoldNote + { + StartTime = 0, + Duration = 1000 + }; + h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + column.Add(note = new DrawableHoldNote(h)); + }); + AddStep("Hold key", () => + { + clock.CurrentTime = 0; + note.OnPressed(ManiaAction.Key1); + }); + AddStep("progress time", () => clock.CurrentTime = 500); + AddAssert("head is visible", () => note.Head.Alpha == 1); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 668487f673..471dad87d5 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -5,13 +5,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Screens; -using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Objects; @@ -414,14 +412,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); - AddUntilStep("wait for head", () => currentPlayer.GameplayClockContainer.GameplayClock.CurrentTime >= time_head); - AddAssert("head is visible", - () => currentPlayer.ChildrenOfType() - .Single(note => note.HitObject == beatmap.HitObjects[0]) - .Head - .Alpha == 1); - - AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor?.HasCompleted.Value == true); } private class ScoreAccessibleReplayPlayer : ReplayPlayer diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index afc08dcc96..9d1f5429a1 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Edit foreach (var line in grid.Objects.OfType()) availableLines.Push(line); - grid.Clear(false); + grid.Clear(); } if (selectionTimeRange == null) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index a0a43ed6ca..dc858fb54f 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -58,8 +58,9 @@ namespace osu.Game.Rulesets.Mania.Edit EditorBeatmap.PerformOnSelection(h => { - if (h is ManiaHitObject maniaObj) - maniaObj.Column += columnDelta; + maniaPlayfield.Remove(h); + ((ManiaHitObject)h).Column += columnDelta; + maniaPlayfield.Add(h); }); } } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index f078345fc1..9aebf51576 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -9,12 +9,6 @@ namespace osu.Game.Rulesets.Mania { public class ManiaSkinComponent : GameplaySkinComponent { - /// - /// The intended index for this component. - /// May be null if the component does not exist in a . - /// - public readonly int? TargetColumn; - /// /// The intended for this component. /// May be null if the component is not a direct member of a . @@ -25,12 +19,10 @@ namespace osu.Game.Rulesets.Mania /// Creates a new . /// /// The component. - /// The intended index for this component. May be null if the component does not exist in a . /// The intended for this component. May be null if the component is not a direct member of a . - public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null) + public ManiaSkinComponent(ManiaSkinComponents component, StageDefinition? stageDefinition = null) : base(component) { - TargetColumn = targetColumn; StageDefinition = stageDefinition; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 02829d87bd..d1310d42eb 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -2,6 +2,7 @@ // 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.Graphics; using osu.Framework.Graphics.Containers; @@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -29,21 +31,21 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public DrawableHoldNoteHead Head => headContainer.Child; public DrawableHoldNoteTail Tail => tailContainer.Child; - private readonly Container headContainer; - private readonly Container tailContainer; - private readonly Container tickContainer; + private Container headContainer; + private Container tailContainer; + private Container tickContainer; /// /// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed. /// - private readonly Container sizingContainer; + private Container sizingContainer; /// /// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of . /// - private readonly Container maskingContainer; + private Container maskingContainer; - private readonly SkinnableDrawable bodyPiece; + private SkinnableDrawable bodyPiece; /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. @@ -60,11 +62,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// private double? releaseTime; + public DrawableHoldNote() + : this(null) + { + } + public DrawableHoldNote(HoldNote hitObject) : base(hitObject) { - RelativeSizeAxes = Axes.X; + } + [BackgroundDependencyLoader] + private void load() + { Container maskedContents; AddRangeInternal(new Drawable[] @@ -86,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables headContainer = new Container { RelativeSizeAxes = Axes.Both } } }, - bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece + bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece { RelativeSizeAxes = Axes.Both, }) @@ -105,6 +115,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }); } + protected override void OnApply() + { + base.OnApply(); + + sizingContainer.Size = Vector2.One; + HoldStartTime = null; + HoldBrokenTime = null; + releaseTime = null; + } + protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); @@ -128,37 +148,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - headContainer.Clear(); - tailContainer.Clear(); - tickContainer.Clear(); + headContainer.Clear(false); + tailContainer.Clear(false); + tickContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) { switch (hitObject) { - case TailNote _: - return new DrawableHoldNoteTail(this) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AccentColour = { BindTarget = AccentColour } - }; + case TailNote tail: + return new DrawableHoldNoteTail(tail); - case Note _: - return new DrawableHoldNoteHead(this) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AccentColour = { BindTarget = AccentColour } - }; + case HeadNote head: + return new DrawableHoldNoteHead(head); case HoldNoteTick tick: - return new DrawableHoldNoteTick(tick) - { - HoldStartTime = () => HoldStartTime, - AccentColour = { BindTarget = AccentColour } - }; + return new DrawableHoldNoteTick(tick); } return base.CreateNestedHitObject(hitObject); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index 35ba2465fa..be600f0d47 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.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 osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Objects.Drawables @@ -12,11 +13,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead; - public DrawableHoldNoteHead(DrawableHoldNote holdNote) - : base(holdNote.HitObject.Head) + public DrawableHoldNoteHead() + : this(null) { } + public DrawableHoldNoteHead(HeadNote headNote) + : base(headNote) + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + } + public void UpdateResult() => base.UpdateResult(true); protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index 3a00933e4d..18aa3f66d4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using osu.Framework.Graphics; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables @@ -20,12 +21,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail; - private readonly DrawableHoldNote holdNote; + protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject; - public DrawableHoldNoteTail(DrawableHoldNote holdNote) - : base(holdNote.HitObject.Tail) + public DrawableHoldNoteTail() + : this(null) { - this.holdNote = holdNote; + } + + public DrawableHoldNoteTail(TailNote tailNote) + : base(tailNote) + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; } public void UpdateResult() => base.UpdateResult(true); @@ -54,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables ApplyResult(r => { // If the head wasn't hit or the hold note was broken, cap the max score to Meh. - if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null)) + if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null)) result = HitResult.Meh; r.Type = result; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index 98931dceed..f040dad135 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -2,7 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK; +using System.Diagnostics; +using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,38 +20,48 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// References the time at which the user started holding the hold note. /// - public Func HoldStartTime; + private Func holdStartTime; + + private Container glowContainer; + + public DrawableHoldNoteTick() + : this(null) + { + } public DrawableHoldNoteTick(HoldNoteTick hitObject) : base(hitObject) { - Container glowContainer; - Anchor = Anchor.TopCentre; Origin = Anchor.TopCentre; RelativeSizeAxes = Axes.X; - Size = new Vector2(1); + } - AddRangeInternal(new[] + [BackgroundDependencyLoader] + private void load() + { + AddInternal(glowContainer = new CircularContainer { - glowContainer = new CircularContainer + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new[] { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true } } }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); AccentColour.BindValueChanged(colour => { @@ -64,12 +75,29 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }, true); } + protected override void OnApply() + { + base.OnApply(); + + Debug.Assert(ParentHitObject != null); + + var holdNote = (DrawableHoldNote)ParentHitObject; + holdStartTime = () => holdNote.HoldStartTime; + } + + protected override void OnFree() + { + base.OnFree(); + + holdStartTime = null; + } + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Time.Current < HitObject.StartTime) return; - var startTime = HoldStartTime?.Invoke(); + var startTime = holdStartTime?.Invoke(); if (startTime == null || startTime > HitObject.StartTime) ApplyResult(r => r.Type = r.Judgement.MinResult); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 003646d654..380ab35339 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -50,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected DrawableManiaHitObject(ManiaHitObject hitObject) : base(hitObject) { + RelativeSizeAxes = Axes.X; } [BackgroundDependencyLoader(true)] @@ -59,9 +60,31 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Action.BindTo(action); Direction.BindTo(scrollingInfo.Direction); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Direction.BindValueChanged(OnDirectionChanged, true); } + protected override void OnApply() + { + base.OnApply(); + + if (ParentHitObject != null) + AccentColour.BindTo(ParentHitObject.AccentColour); + } + + protected override void OnFree() + { + base.OnFree(); + + if (ParentHitObject != null) + AccentColour.UnbindFrom(ParentHitObject.AccentColour); + } + private double computedLifetimeStart; public override double LifetimeStart @@ -147,12 +170,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public abstract class DrawableManiaHitObject : DrawableManiaHitObject where TObject : ManiaHitObject { - public new readonly TObject HitObject; + public new TObject HitObject => (TObject)base.HitObject; protected DrawableManiaHitObject(TObject hitObject) : base(hitObject) { - HitObject = hitObject; } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 36565e14aa..33d872dfb6 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -33,31 +33,37 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note; - private readonly Drawable headPiece; + private Drawable headPiece; + + public DrawableNote() + : this(null) + { + } public DrawableNote(Note hitObject) : base(hitObject) { - RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - - AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component, hitObject.Column), _ => new DefaultNotePiece()) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }); } [BackgroundDependencyLoader(true)] private void load(ManiaRulesetConfigManager rulesetConfig) { rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring); + + AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component), _ => new DefaultNotePiece()) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }); } protected override void LoadComplete() { - HitObject.StartTimeBindable.BindValueChanged(_ => updateSnapColour()); - configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour(), true); + base.LoadComplete(); + + configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour()); + StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true); } protected override void OnDirectionChanged(ValueChangedEvent e) @@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private void updateSnapColour() { - if (beatmap == null) return; + if (beatmap == null || HitObject == null) return; int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime); diff --git a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs new file mode 100644 index 0000000000..e69cc62aed --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs @@ -0,0 +1,9 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Mania.Objects +{ + public class HeadNote : Note + { + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 6cc7ff92d3..43e876b7aa 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// The head note of the hold. /// - public Note Head { get; private set; } + public HeadNote Head { get; private set; } /// /// The tail note of the hold. @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Objects createTicks(cancellationToken); - AddNested(Head = new Note + AddNested(Head = new HeadNote { StartTime = StartTime, Column = Column, diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 0f02e2cd4b..9b5893b268 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI @@ -55,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y; Width = COLUMN_WIDTH; - Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground()) + Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) { RelativeSizeAxes = Axes.Both }; @@ -66,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.UI // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements background.CreateProxy(), HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea()) + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) { RelativeSizeAxes = Axes.Both }, @@ -83,6 +84,19 @@ namespace osu.Game.Rulesets.Mania.UI hitPolicy = new OrderedHitPolicy(HitObjectContainer); TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); + + RegisterPool(10, 50); + RegisterPool(10, 50); + RegisterPool(10, 50); + RegisterPool(10, 50); + RegisterPool(50, 250); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + NewResult += OnNewResult; } public ColumnType ColumnType { get; set; } @@ -98,28 +112,14 @@ namespace osu.Game.Rulesets.Mania.UI return dependencies; } - /// - /// Adds a DrawableHitObject to this Playfield. - /// - /// The DrawableHitObject to add. - public override void Add(DrawableHitObject hitObject) + protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) { - hitObject.AccentColour.Value = AccentColour; - hitObject.OnNewResult += OnNewResult; + base.OnNewDrawableHitObject(drawableHitObject); - DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject; + DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)drawableHitObject; + + maniaObject.AccentColour.Value = AccentColour; maniaObject.CheckHittable = hitPolicy.IsHittable; - - base.Add(hitObject); - } - - public override bool Remove(DrawableHitObject h) - { - if (!base.Remove(h)) - return false; - - h.OnNewResult -= OnNewResult; - return true; } internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index b365ae45a9..f69d2aafdc 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components RelativeSizeAxes = Axes.Both, Depth = 2, }, - hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget()) + hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget()) { RelativeSizeAxes = Axes.X, Depth = 1 diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 4ee060e91e..e497646a13 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -18,7 +18,6 @@ using osu.Game.Replays; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -134,20 +133,7 @@ namespace osu.Game.Rulesets.Mania.UI protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant); - public override DrawableHitObject CreateDrawableRepresentation(ManiaHitObject h) - { - switch (h) - { - case HoldNote holdNote: - return new DrawableHoldNote(holdNote); - - case Note note: - return new DrawableNote(note); - - default: - return null; - } - } + public override DrawableHitObject CreateDrawableRepresentation(ManiaHitObject h) => null; protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 271e432e8d..8830c440c0 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -9,6 +9,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osuTK; @@ -56,6 +57,10 @@ namespace osu.Game.Rulesets.Mania.UI } } + public override void Add(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Add(hitObject); + + public override bool Remove(HitObject hitObject) => getStageByColumn(((ManiaHitObject)hitObject).Column).Remove(hitObject); + public override void Add(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Add(h); public override bool Remove(DrawableHitObject h) => getStageByColumn(((ManiaHitObject)h.HitObject).Column).Remove(h); diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs index 64b7d7d550..90d3c6c4c7 100644 --- a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.UI [BackgroundDependencyLoader] private void load() { - InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), _ => new DefaultHitExplosion()) + InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), _ => new DefaultHitExplosion()) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index dc34bffab1..8c703e7a8a 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI.Components; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -132,33 +133,19 @@ namespace osu.Game.Rulesets.Mania.UI } } - public override void Add(DrawableHitObject h) + protected override void LoadComplete() { - var maniaObject = (ManiaHitObject)h.HitObject; - - int columnIndex = -1; - - maniaObject.ColumnBindable.BindValueChanged(_ => - { - if (columnIndex != -1) - Columns.ElementAt(columnIndex).Remove(h); - - columnIndex = maniaObject.Column - firstColumnIndex; - Columns.ElementAt(columnIndex).Add(h); - }, true); - - h.OnNewResult += OnNewResult; + base.LoadComplete(); + NewResult += OnNewResult; } - public override bool Remove(DrawableHitObject h) - { - var maniaObject = (ManiaHitObject)h.HitObject; - int columnIndex = maniaObject.Column - firstColumnIndex; - Columns.ElementAt(columnIndex).Remove(h); + public override void Add(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Add(hitObject); - h.OnNewResult -= OnNewResult; - return true; - } + public override bool Remove(HitObject hitObject) => Columns.ElementAt(((ManiaHitObject)hitObject).Column - firstColumnIndex).Remove(hitObject); + + public override void Add(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Add(h); + + public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h); public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline)); diff --git a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs index a1d000386f..ac01508081 100644 --- a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [SetUpSteps] public void SetUp() - => AddStep("clear SHOC", () => hitObjectContainer.Clear(false)); + => AddStep("clear SHOC", () => hitObjectContainer.Clear()); protected void AddHitObject(DrawableHitObject hitObject) => AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject)); diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectContainerEventBuffer.cs b/osu.Game.Tests/Editing/TestSceneHitObjectContainerEventBuffer.cs index 5233cbc0be..592971dbaf 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectContainerEventBuffer.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectContainerEventBuffer.cs @@ -4,6 +4,7 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -12,6 +13,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Editing { + [HeadlessTest] public class TestSceneHitObjectContainerEventBuffer : OsuTestScene { private readonly TestHitObject testObj = new TestHitObject(); diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index 2e3f192f1b..0bec02c488 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -45,15 +45,16 @@ namespace osu.Game.Tests.Gameplay AddStep("Create DHO", () => { dho = new TestDrawableHitObject(null); - dho.Apply(entry = new TestLifetimeEntry(new HitObject()) - { - LifetimeStart = 0, - LifetimeEnd = 1000, - }); + dho.Apply(entry = new TestLifetimeEntry(new HitObject())); Child = dho; }); - AddStep("KeepAlive = true", () => entry.KeepAlive = true); + AddStep("KeepAlive = true", () => + { + entry.LifetimeStart = 0; + entry.LifetimeEnd = 1000; + entry.KeepAlive = true; + }); AddAssert("Lifetime is overriden", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == double.MaxValue); AddStep("Set LifetimeStart", () => dho.LifetimeStart = 500); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs new file mode 100644 index 0000000000..48b5e67814 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -0,0 +1,133 @@ +// 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; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Framework.Lists; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Extensions; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Skinning.Legacy; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneBeatmapSkinFallbacks : OsuPlayerTestScene + { + private ISkin currentBeatmapSkin; + + [Resolved] + private SkinManager skinManager { get; set; } + + [Cached] + private ScoreProcessor scoreProcessor = new ScoreProcessor(); + + [Cached(typeof(HealthProcessor))] + private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); + + protected override bool HasCustomSteps => true; + + [Test] + public void TestEmptyDefaultBeatmapSkinFallsBack() + { + CreateSkinTest(DefaultLegacySkin.Info, () => new TestWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)).Skin); + AddAssert("hud from default legacy skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); + } + + [Test] + public void TestEmptyLegacyBeatmapSkinFallsBack() + { + CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null)); + AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); + } + + protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func getBeatmapSkin) + { + CreateTest(() => + { + AddStep("setup skins", () => + { + skinManager.CurrentSkinInfo.Value = gameCurrentSkin; + currentBeatmapSkin = getBeatmapSkin(); + }); + }); + } + + protected bool AssertComponentsFromExpectedSource(SkinnableTarget target, ISkin expectedSource) + { + var expectedComponentsContainer = (SkinnableTargetComponentsContainer)expectedSource.GetDrawableComponent(new SkinnableTargetComponent(target)); + + Add(expectedComponentsContainer); + expectedComponentsContainer?.UpdateSubTree(); + var expectedInfo = expectedComponentsContainer?.CreateSkinnableInfo(); + Remove(expectedComponentsContainer); + + var actualInfo = Player.ChildrenOfType().First(s => s.Target == target) + .ChildrenOfType().Single().CreateSkinnableInfo(); + + return almostEqual(actualInfo, expectedInfo, 2f); + + static bool almostEqual(SkinnableInfo info, SkinnableInfo other, float positionTolerance) => + other != null + && info.Anchor == other.Anchor + && info.Origin == other.Origin + && Precision.AlmostEquals(info.Position, other.Position, positionTolerance) + && info.Children.SequenceEqual(other.Children, new FuncEqualityComparer((s1, s2) => almostEqual(s1, s2, positionTolerance))); + } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin); + + protected override Ruleset CreatePlayerRuleset() => new TestOsuRuleset(); + + private class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap + { + private readonly ISkin beatmapSkin; + + public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, ISkin beatmapSkin) + : base(beatmap, storyboard, referenceClock, audio) + { + this.beatmapSkin = beatmapSkin; + } + + protected override ISkin GetSkin() => beatmapSkin; + } + + private class TestOsuRuleset : OsuRuleset + { + public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(source); + + private class TestOsuLegacySkinTransformer : OsuLegacySkinTransformer + { + public TestOsuLegacySkinTransformer(ISkinSource source) + : base(source) + { + } + + public override Drawable GetDrawableComponent(ISkinComponent component) + { + var drawable = base.GetDrawableComponent(component); + if (drawable != null) + return drawable; + + // this isn't really supposed to make a difference from returning null, + // but it appears it does, returning null will skip over falling back to beatmap skin, + // while calling Source.GetDrawableComponent() doesn't. + return Source.GetDrawableComponent(component); + } + } + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 6cefd01aab..2c5443fe08 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,29 +14,35 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Play.HUD.HitErrorMeters; namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneHitErrorMeter : OsuTestScene { - private HitWindows hitWindows; - [Cached] private ScoreProcessor scoreProcessor = new ScoreProcessor(); + [Cached(typeof(DrawableRuleset))] + private TestDrawableRuleset drawableRuleset = new TestDrawableRuleset(); + public TestSceneHitErrorMeter() { recreateDisplay(new OsuHitWindows(), 5); AddRepeatStep("New random judgement", () => newJudgement(), 40); - AddRepeatStep("New max negative", () => newJudgement(-hitWindows.WindowFor(HitResult.Meh)), 20); - AddRepeatStep("New max positive", () => newJudgement(hitWindows.WindowFor(HitResult.Meh)), 20); + AddRepeatStep("New max negative", () => newJudgement(-drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20); + AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20); AddStep("New fixed judgement (50ms)", () => newJudgement(50)); AddStep("Judgement barrage", () => @@ -83,10 +92,10 @@ namespace osu.Game.Tests.Visual.Gameplay private void recreateDisplay(HitWindows hitWindows, float overallDifficulty) { - this.hitWindows = hitWindows; - hitWindows?.SetDifficulty(overallDifficulty); + drawableRuleset.HitWindows = hitWindows; + Clear(); Add(new FillFlowContainer @@ -103,40 +112,40 @@ namespace osu.Game.Tests.Visual.Gameplay } }); - Add(new BarHitErrorMeter(hitWindows, true) + Add(new BarHitErrorMeter { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, }); - Add(new BarHitErrorMeter(hitWindows, false) + Add(new BarHitErrorMeter { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }); - Add(new BarHitErrorMeter(hitWindows, true) + Add(new BarHitErrorMeter { Anchor = Anchor.BottomCentre, Origin = Anchor.CentreLeft, Rotation = 270, }); - Add(new ColourHitErrorMeter(hitWindows) + Add(new ColourHitErrorMeter { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding { Right = 50 } }); - Add(new ColourHitErrorMeter(hitWindows) + Add(new ColourHitErrorMeter { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Margin = new MarginPadding { Left = 50 } }); - Add(new ColourHitErrorMeter(hitWindows) + Add(new ColourHitErrorMeter { Anchor = Anchor.BottomCentre, Origin = Anchor.CentreLeft, @@ -147,11 +156,47 @@ namespace osu.Game.Tests.Visual.Gameplay private void newJudgement(double offset = 0) { - scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement()) + scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = drawableRuleset.HitWindows }, new Judgement()) { TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset, Type = HitResult.Perfect, }); } + + [SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")] + private class TestDrawableRuleset : DrawableRuleset + { + public HitWindows HitWindows; + + public override IEnumerable Objects => new[] { new HitCircle { HitWindows = HitWindows } }; + + public override event Action NewResult; + public override event Action RevertResult; + + public override Playfield Playfield { get; } + public override Container Overlays { get; } + public override Container FrameStableComponents { get; } + public override IFrameStableClock FrameStableClock { get; } + public override IReadOnlyList Mods { get; } + + public override double GameplayStartTime { get; } + public override GameplayCursorContainer Cursor { get; } + + public TestDrawableRuleset() + : base(new OsuRuleset()) + { + // won't compile without this. + NewResult?.Invoke(null); + RevertResult?.Invoke(null); + } + + public override void SetReplayScore(Score replayScore) => throw new NotImplementedException(); + + public override void SetRecordTarget(Score score) => throw new NotImplementedException(); + + public override void RequestResume(Action continueResume) => throw new NotImplementedException(); + + public override void CancelResume() => throw new NotImplementedException(); + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsSidebar.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsSidebar.cs new file mode 100644 index 0000000000..b000553a7b --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsSidebar.cs @@ -0,0 +1,152 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.News.Sidebar; +using static osu.Game.Overlays.News.Sidebar.YearsPanel; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneNewsSidebar : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + private TestNewsSidebar sidebar; + + [SetUp] + public void SetUp() => Schedule(() => Child = sidebar = new TestNewsSidebar { YearChanged = onYearChanged }); + + [Test] + public void TestBasic() + { + AddStep("Add metadata", () => sidebar.Metadata.Value = getMetadata(2021)); + AddUntilStep("Month sections exist", () => sidebar.ChildrenOfType().Any()); + } + + [Test] + public void TestMetadataWithNoPosts() + { + AddStep("Add data with no posts", () => sidebar.Metadata.Value = metadata_with_no_posts); + AddUntilStep("No month sections were created", () => !sidebar.ChildrenOfType().Any()); + } + + [Test] + public void TestYearsPanelVisibility() + { + AddUntilStep("Years panel is hidden", () => yearsPanel?.Alpha == 0); + AddStep("Add data", () => sidebar.Metadata.Value = getMetadata(2021)); + AddUntilStep("Years panel is visible", () => yearsPanel?.Alpha == 1); + } + + private void onYearChanged(int year) => sidebar.Metadata.Value = getMetadata(year); + + private YearsPanel yearsPanel => sidebar.ChildrenOfType().FirstOrDefault(); + + private APINewsSidebar getMetadata(int year) => new APINewsSidebar + { + CurrentYear = year, + Years = new[] + { + 2021, + 2020, + 2019, + 2018, + 2017, + 2016, + 2015, + 2014, + 2013 + }, + NewsPosts = new List + { + new APINewsPost + { + Title = "(Mar) Short title", + PublishedAt = new DateTime(year, 3, 1) + }, + new APINewsPost + { + Title = "(Mar) Oh boy that's a long post title I wonder if it will break anything", + PublishedAt = new DateTime(year, 3, 1) + }, + new APINewsPost + { + Title = "(Mar) Medium title, nothing to see here", + PublishedAt = new DateTime(year, 3, 1) + }, + new APINewsPost + { + Title = "(Feb) Short title", + PublishedAt = new DateTime(year, 2, 1) + }, + new APINewsPost + { + Title = "(Feb) Oh boy that's a long post title I wonder if it will break anything", + PublishedAt = new DateTime(year, 2, 1) + }, + new APINewsPost + { + Title = "(Feb) Medium title, nothing to see here", + PublishedAt = new DateTime(year, 2, 1) + }, + new APINewsPost + { + Title = "Short title", + PublishedAt = new DateTime(year, 1, 1) + }, + new APINewsPost + { + Title = "Oh boy that's a long post title I wonder if it will break anything", + PublishedAt = new DateTime(year, 1, 1) + }, + new APINewsPost + { + Title = "Medium title, nothing to see here", + PublishedAt = new DateTime(year, 1, 1) + } + } + }; + + private static readonly APINewsSidebar metadata_with_no_posts = new APINewsSidebar + { + CurrentYear = 2021, + Years = new[] + { + 2021, + 2020, + 2019, + 2018, + 2017, + 2016, + 2015, + 2014, + 2013 + }, + NewsPosts = Array.Empty() + }; + + private class TestNewsSidebar : NewsSidebar + { + public Action YearChanged; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Metadata.BindValueChanged(metadata => + { + foreach (var b in this.ChildrenOfType()) + b.Action = () => YearChanged?.Invoke(b.Year); + }, true); + } + } + } +} diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 3576b149bf..ead8572c54 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -324,7 +324,7 @@ namespace osu.Game.Beatmaps public bool SkinLoaded => skin.IsResultAvailable; public ISkin Skin => skin.Value; - protected virtual ISkin GetSkin() => new DefaultSkin(null); + protected virtual ISkin GetSkin() => new BeatmapSkin(BeatmapInfo); private readonly RecyclableLazy skin; public abstract Stream GetStream(string storagePath); diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 09412b1f1b..43bbd725c3 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -104,7 +104,6 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.KeyOverlay, false); SetDefault(OsuSetting.PositionalHitSounds, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); - SetDefault(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); SetDefault(OsuSetting.FloatingComments, false); @@ -213,7 +212,6 @@ namespace osu.Game.Configuration KeyOverlay, PositionalHitSounds, AlwaysPlayFirstComboBreak, - ScoreMeter, FloatingComments, HUDVisibilityMode, ShowProgressGraph, diff --git a/osu.Game/Configuration/ScoreMeterType.cs b/osu.Game/Configuration/ScoreMeterType.cs deleted file mode 100644 index ddbd2327c2..0000000000 --- a/osu.Game/Configuration/ScoreMeterType.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.ComponentModel; - -namespace osu.Game.Configuration -{ - public enum ScoreMeterType - { - [Description("None")] - None, - - [Description("Hit Error (left)")] - HitErrorLeft, - - [Description("Hit Error (right)")] - HitErrorRight, - - [Description("Hit Error (left+right)")] - HitErrorBoth, - - [Description("Hit Error (bottom)")] - HitErrorBottom, - - [Description("Colour (left)")] - ColourLeft, - - [Description("Colour (right)")] - ColourRight, - - [Description("Colour (left+right)")] - ColourBoth, - - [Description("Colour (bottom)")] - ColourBottom, - } -} diff --git a/osu.Game/Online/API/Requests/GetNewsResponse.cs b/osu.Game/Online/API/Requests/GetNewsResponse.cs index 835289a51d..98f76d105c 100644 --- a/osu.Game/Online/API/Requests/GetNewsResponse.cs +++ b/osu.Game/Online/API/Requests/GetNewsResponse.cs @@ -11,5 +11,8 @@ namespace osu.Game.Online.API.Requests { [JsonProperty("news_posts")] public IEnumerable NewsPosts; + + [JsonProperty("news_sidebar")] + public APINewsSidebar SidebarMetadata; } } diff --git a/osu.Game/Online/API/Requests/Responses/APINewsSidebar.cs b/osu.Game/Online/API/Requests/Responses/APINewsSidebar.cs new file mode 100644 index 0000000000..b8d6469a1d --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APINewsSidebar.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APINewsSidebar + { + [JsonProperty("current_year")] + public int CurrentYear { get; set; } + + [JsonProperty("news_posts")] + public IEnumerable NewsPosts { get; set; } + + [JsonProperty("years")] + public int[] Years { get; set; } + } +} diff --git a/osu.Game/Overlays/News/Sidebar/MonthSection.cs b/osu.Game/Overlays/News/Sidebar/MonthSection.cs new file mode 100644 index 0000000000..b300a755f9 --- /dev/null +++ b/osu.Game/Overlays/News/Sidebar/MonthSection.cs @@ -0,0 +1,179 @@ +// 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.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Graphics.Containers; +using osuTK; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics; +using System.Linq; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using System.Diagnostics; +using osu.Framework.Platform; + +namespace osu.Game.Overlays.News.Sidebar +{ + public class MonthSection : CompositeDrawable + { + private const int animation_duration = 250; + + public readonly BindableBool Expanded = new BindableBool(); + + public MonthSection(int month, int year, IEnumerable posts) + { + Debug.Assert(posts.All(p => p.PublishedAt.Month == month && p.PublishedAt.Year == year)); + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Masking = true; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DropdownHeader(month, year) + { + Expanded = { BindTarget = Expanded } + }, + new PostsContainer + { + Expanded = { BindTarget = Expanded }, + Children = posts.Select(p => new PostButton(p)).ToArray() + } + } + }; + } + + private class DropdownHeader : OsuClickableContainer + { + public readonly BindableBool Expanded = new BindableBool(); + + private readonly SpriteIcon icon; + + public DropdownHeader(int month, int year) + { + var date = new DateTime(year, month, 1); + + RelativeSizeAxes = Axes.X; + Height = 15; + Action = Expanded.Toggle; + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Text = date.ToString("MMM yyyy") + }, + icon = new SpriteIcon + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(10), + Icon = FontAwesome.Solid.ChevronDown + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(open => + { + icon.Scale = new Vector2(1, open.NewValue ? -1 : 1); + }, true); + } + } + + private class PostButton : OsuHoverContainer + { + protected override IEnumerable EffectTargets => new[] { text }; + + private readonly TextFlowContainer text; + private readonly APINewsPost post; + + public PostButton(APINewsPost post) + { + this.post = post; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Child = text = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = post.Title + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider overlayColours, GameHost host) + { + IdleColour = overlayColours.Light2; + HoverColour = overlayColours.Light1; + + TooltipText = "view in browser"; + Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); + } + } + + private class PostsContainer : Container + { + public readonly BindableBool Expanded = new BindableBool(); + + protected override Container Content { get; } + + public PostsContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + AutoSizeDuration = animation_duration; + AutoSizeEasing = Easing.Out; + InternalChild = Content = new FillFlowContainer + { + Margin = new MarginPadding { Top = 5 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Alpha = 0 + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Expanded.BindValueChanged(updateState, true); + } + + private void updateState(ValueChangedEvent expanded) + { + ClearTransforms(true); + + if (expanded.NewValue) + { + AutoSizeAxes = Axes.Y; + Content.FadeIn(animation_duration, Easing.OutQuint); + } + else + { + AutoSizeAxes = Axes.None; + this.ResizeHeightTo(0, animation_duration, Easing.OutQuint); + + Content.FadeOut(animation_duration, Easing.OutQuint); + } + } + } + } +} diff --git a/osu.Game/Overlays/News/Sidebar/NewsSidebar.cs b/osu.Game/Overlays/News/Sidebar/NewsSidebar.cs new file mode 100644 index 0000000000..d14ad90ef4 --- /dev/null +++ b/osu.Game/Overlays/News/Sidebar/NewsSidebar.cs @@ -0,0 +1,103 @@ +// 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.Containers; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Framework.Graphics.Shapes; +using osuTK; +using System.Linq; + +namespace osu.Game.Overlays.News.Sidebar +{ + public class NewsSidebar : CompositeDrawable + { + [Cached] + public readonly Bindable Metadata = new Bindable(); + + private FillFlowContainer monthsFlow; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.Y; + Width = 250; + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Vertical = 20, + Left = 50, + Right = 30 + }, + Child = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 20), + Children = new Drawable[] + { + new YearsPanel(), + monthsFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10) + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Metadata.BindValueChanged(onMetadataChanged, true); + } + + private void onMetadataChanged(ValueChangedEvent metadata) + { + monthsFlow.Clear(); + + if (metadata.NewValue == null) + return; + + var allPosts = metadata.NewValue.NewsPosts; + + if (allPosts?.Any() != true) + return; + + var lookup = metadata.NewValue.NewsPosts.ToLookup(post => post.PublishedAt.Month); + + var keys = lookup.Select(kvp => kvp.Key); + var sortedKeys = keys.OrderByDescending(k => k).ToList(); + + var year = metadata.NewValue.CurrentYear; + + for (int i = 0; i < sortedKeys.Count; i++) + { + var month = sortedKeys[i]; + var posts = lookup[month]; + + monthsFlow.Add(new MonthSection(month, year, posts) + { + Expanded = { Value = i == 0 } + }); + } + } + } +} diff --git a/osu.Game/Overlays/News/Sidebar/YearsPanel.cs b/osu.Game/Overlays/News/Sidebar/YearsPanel.cs new file mode 100644 index 0000000000..b6bbdbb6d4 --- /dev/null +++ b/osu.Game/Overlays/News/Sidebar/YearsPanel.cs @@ -0,0 +1,113 @@ +// 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.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.News.Sidebar +{ + public class YearsPanel : CompositeDrawable + { + private readonly Bindable metadata = new Bindable(); + + private FillFlowContainer yearsFlow; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider overlayColours, Bindable metadata) + { + this.metadata.BindTo(metadata); + + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + Masking = true; + CornerRadius = 6; + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = overlayColours.Background3 + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(5), + Child = yearsFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 5) + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + metadata.BindValueChanged(_ => recreateDrawables(), true); + } + + private void recreateDrawables() + { + yearsFlow.Clear(); + + if (metadata.Value == null) + { + Hide(); + return; + } + + var currentYear = metadata.Value.CurrentYear; + + foreach (var y in metadata.Value.Years) + yearsFlow.Add(new YearButton(y, y == currentYear)); + + Show(); + } + + public class YearButton : OsuHoverContainer + { + public int Year { get; } + + private readonly bool isCurrent; + + public YearButton(int year, bool isCurrent) + { + Year = year; + this.isCurrent = isCurrent; + + RelativeSizeAxes = Axes.X; + Width = 0.25f; + Height = 15; + + Child = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 12, weight: isCurrent ? FontWeight.SemiBold : FontWeight.Medium), + Text = year.ToString() + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IdleColour = isCurrent ? Color4.White : colourProvider.Light2; + HoverColour = isCurrent ? Color4.White : colourProvider.Light1; + Action = () => { }; // Avoid button being disabled since there's no proper action assigned. + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index be464fa2b7..0b5ec4f338 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -73,11 +73,6 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Always play first combo break sound", Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) }, - new SettingsEnumDropdown - { - LabelText = "Score meter type", - Current = config.GetBindable(OsuSetting.ScoreMeter) - }, new SettingsEnumDropdown { LabelText = "Score display mode", diff --git a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs index 93e476be76..ed0430012a 100644 --- a/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs +++ b/osu.Game/Rulesets/Objects/Pooling/PoolableDrawableWithLifetime.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Diagnostics; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Objects.Pooling /// /// The entry holding essential state of this . /// - protected TEntry? Entry { get; private set; } + public TEntry? Entry { get; private set; } /// /// Whether is applied to this . @@ -28,14 +29,28 @@ namespace osu.Game.Rulesets.Objects.Pooling public override double LifetimeStart { - get => base.LifetimeStart; - set => setLifetime(value, LifetimeEnd); + get => Entry?.LifetimeStart ?? double.MinValue; + set + { + if (Entry == null && LifetimeStart != value) + throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime)} when entry is not set"); + + if (Entry != null) + Entry.LifetimeStart = value; + } } public override double LifetimeEnd { - get => base.LifetimeEnd; - set => setLifetime(LifetimeStart, value); + get => Entry?.LifetimeEnd ?? double.MaxValue; + set + { + if (Entry == null && LifetimeEnd != value) + throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime)} when entry is not set"); + + if (Entry != null) + Entry.LifetimeEnd = value; + } } public override bool RemoveWhenNotAlive => false; @@ -64,11 +79,8 @@ namespace osu.Game.Rulesets.Objects.Pooling if (HasEntryApplied) free(); - setLifetime(entry.LifetimeStart, entry.LifetimeEnd); Entry = entry; - OnApply(entry); - HasEntryApplied = true; } @@ -95,27 +107,12 @@ namespace osu.Game.Rulesets.Objects.Pooling { } - private void setLifetime(double start, double end) - { - base.LifetimeStart = start; - base.LifetimeEnd = end; - - if (Entry != null) - { - Entry.LifetimeStart = start; - Entry.LifetimeEnd = end; - } - } - private void free() { Debug.Assert(Entry != null && HasEntryApplied); OnFree(Entry); - Entry = null; - setLifetime(double.MaxValue, double.MaxValue); - HasEntryApplied = false; } } diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 11312a46df..dcf350cbd4 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -17,8 +17,18 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.UI { - public class HitObjectContainer : LifetimeManagementContainer, IHitObjectContainer + public class HitObjectContainer : CompositeDrawable, IHitObjectContainer { + /// + /// All entries in this including dead entries. + /// + public IEnumerable Entries => allEntries; + + /// + /// All alive entries and s used by the entries. + /// + public IEnumerable<(HitObjectLifetimeEntry Entry, DrawableHitObject Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value)); + public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); @@ -60,8 +70,12 @@ namespace osu.Game.Rulesets.UI internal double FutureLifetimeExtension { get; set; } private readonly Dictionary startTimeMap = new Dictionary(); - private readonly Dictionary drawableMap = new Dictionary(); + + private readonly Dictionary aliveDrawableMap = new Dictionary(); + private readonly Dictionary nonPooledDrawableMap = new Dictionary(); + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + private readonly HashSet allEntries = new HashSet(); [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } @@ -72,6 +86,7 @@ namespace osu.Game.Rulesets.UI lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; + lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; } protected override void LoadAsyncComplete() @@ -84,93 +99,113 @@ namespace osu.Game.Rulesets.UI #region Pooling support - public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry); - - public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry); - - private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry); - - private void entryBecameDead(LifetimeEntry entry) => removeDrawable((HitObjectLifetimeEntry)entry); - - private void addDrawable(HitObjectLifetimeEntry entry) + public void Add(HitObjectLifetimeEntry entry) { - Debug.Assert(!drawableMap.ContainsKey(entry)); + allEntries.Add(entry); + lifetimeManager.AddEntry(entry); + } - var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null); + public bool Remove(HitObjectLifetimeEntry entry) + { + if (!lifetimeManager.RemoveEntry(entry)) return false; + + // This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry. + if (nonPooledDrawableMap.Remove(entry, out var drawable)) + removeDrawable(drawable); + + allEntries.Remove(entry); + return true; + } + + private void entryBecameAlive(LifetimeEntry lifetimeEntry) + { + var entry = (HitObjectLifetimeEntry)lifetimeEntry; + Debug.Assert(!aliveDrawableMap.ContainsKey(entry)); + + bool isNonPooled = nonPooledDrawableMap.TryGetValue(entry, out var drawable); + drawable ??= pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null); if (drawable == null) throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); + aliveDrawableMap[entry] = drawable; + OnAdd(drawable); + + if (isNonPooled) return; + + addDrawable(drawable); + HitObjectUsageBegan?.Invoke(entry.HitObject); + } + + private void entryBecameDead(LifetimeEntry lifetimeEntry) + { + var entry = (HitObjectLifetimeEntry)lifetimeEntry; + Debug.Assert(aliveDrawableMap.ContainsKey(entry)); + + var drawable = aliveDrawableMap[entry]; + bool isNonPooled = nonPooledDrawableMap.ContainsKey(entry); + + drawable.OnKilled(); + aliveDrawableMap.Remove(entry); + OnRemove(drawable); + + if (isNonPooled) return; + + removeDrawable(drawable); + // The hit object is not freed when the DHO was not pooled. + HitObjectUsageFinished?.Invoke(entry.HitObject); + } + + private void addDrawable(DrawableHitObject drawable) + { drawable.OnNewResult += onNewResult; drawable.OnRevertResult += onRevertResult; bindStartTime(drawable); - AddInternal(drawableMap[entry] = drawable, false); - OnAdd(drawable); - - HitObjectUsageBegan?.Invoke(entry.HitObject); + AddInternal(drawable); } - private void removeDrawable(HitObjectLifetimeEntry entry) + private void removeDrawable(DrawableHitObject drawable) { - Debug.Assert(drawableMap.ContainsKey(entry)); - - var drawable = drawableMap[entry]; - - // OnKilled can potentially change the hitobject's result, so it needs to run first before unbinding. - drawable.OnKilled(); drawable.OnNewResult -= onNewResult; drawable.OnRevertResult -= onRevertResult; - drawableMap.Remove(entry); - - OnRemove(drawable); unbindStartTime(drawable); - RemoveInternal(drawable); - HitObjectUsageFinished?.Invoke(entry.HitObject); + RemoveInternal(drawable); } #endregion #region Non-pooling support - public virtual void Add(DrawableHitObject hitObject) + public virtual void Add(DrawableHitObject drawable) { - bindStartTime(hitObject); + if (drawable.Entry == null) + throw new InvalidOperationException($"May not add a {nameof(DrawableHitObject)} without {nameof(HitObject)} associated"); - hitObject.OnNewResult += onNewResult; - hitObject.OnRevertResult += onRevertResult; - - AddInternal(hitObject); - OnAdd(hitObject); + nonPooledDrawableMap.Add(drawable.Entry, drawable); + addDrawable(drawable); + Add(drawable.Entry); } - public virtual bool Remove(DrawableHitObject hitObject) + public virtual bool Remove(DrawableHitObject drawable) { - OnRemove(hitObject); - if (!RemoveInternal(hitObject)) + if (drawable.Entry == null) return false; - hitObject.OnNewResult -= onNewResult; - hitObject.OnRevertResult -= onRevertResult; - - unbindStartTime(hitObject); - - return true; + return Remove(drawable.Entry); } public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); - protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) + private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction) { - if (!(e.Child is DrawableHitObject hitObject)) - return; + if (nonPooledDrawableMap.TryGetValue((HitObjectLifetimeEntry)entry, out var drawable)) + OnChildLifetimeBoundaryCrossed(new LifetimeBoundaryCrossedEvent(drawable, kind, direction)); + } - if ((e.Kind == LifetimeBoundaryKind.End && e.Direction == LifetimeBoundaryCrossingDirection.Forward) - || (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward)) - { - hitObject.OnKilled(); - } + protected virtual void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) + { } #endregion @@ -195,12 +230,13 @@ namespace osu.Game.Rulesets.UI { } - public virtual void Clear(bool disposeChildren = true) + public virtual void Clear() { lifetimeManager.ClearEntries(); - - ClearInternal(disposeChildren); - unbindAllStartTimes(); + foreach (var drawable in nonPooledDrawableMap.Values) + removeDrawable(drawable); + nonPooledDrawableMap.Clear(); + Debug.Assert(InternalChildren.Count == 0 && startTimeMap.Count == 0 && aliveDrawableMap.Count == 0, "All hit objects should have been removed"); } protected override bool CheckChildrenLife() diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 289578f3d8..a9eaf3da68 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -50,9 +50,9 @@ namespace osu.Game.Rulesets.UI.Scrolling timeRange.ValueChanged += _ => layoutCache.Invalidate(); } - public override void Clear(bool disposeChildren = true) + public override void Clear() { - base.Clear(disposeChildren); + base.Clear(); toComputeLifetime.Clear(); layoutComputed.Clear(); diff --git a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs deleted file mode 100644 index a24d9c10cb..0000000000 --- a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs +++ /dev/null @@ -1,127 +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 osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Configuration; -using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD.HitErrorMeters; - -namespace osu.Game.Screens.Play.HUD -{ - public class HitErrorDisplay : Container - { - private const int fade_duration = 200; - private const int margin = 10; - - private readonly Bindable type = new Bindable(); - - private readonly HitWindows hitWindows; - - public HitErrorDisplay(HitWindows hitWindows) - { - this.hitWindows = hitWindows; - - RelativeSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - config.BindWith(OsuSetting.ScoreMeter, type); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - type.BindValueChanged(typeChanged, true); - } - - private void typeChanged(ValueChangedEvent type) - { - Children.ForEach(c => c.FadeOut(fade_duration, Easing.OutQuint)); - - if (hitWindows == null) - return; - - switch (type.NewValue) - { - case ScoreMeterType.HitErrorBoth: - createBar(Anchor.CentreLeft); - createBar(Anchor.CentreRight); - break; - - case ScoreMeterType.HitErrorLeft: - createBar(Anchor.CentreLeft); - break; - - case ScoreMeterType.HitErrorRight: - createBar(Anchor.CentreRight); - break; - - case ScoreMeterType.HitErrorBottom: - createBar(Anchor.BottomCentre); - break; - - case ScoreMeterType.ColourBoth: - createColour(Anchor.CentreLeft); - createColour(Anchor.CentreRight); - break; - - case ScoreMeterType.ColourLeft: - createColour(Anchor.CentreLeft); - break; - - case ScoreMeterType.ColourRight: - createColour(Anchor.CentreRight); - break; - - case ScoreMeterType.ColourBottom: - createColour(Anchor.BottomCentre); - break; - } - } - - private void createBar(Anchor anchor) - { - bool rightAligned = (anchor & Anchor.x2) > 0; - bool bottomAligned = (anchor & Anchor.y2) > 0; - - var display = new BarHitErrorMeter(hitWindows, rightAligned) - { - Margin = new MarginPadding(margin), - Anchor = anchor, - Origin = bottomAligned ? Anchor.CentreLeft : anchor, - Alpha = 0, - Rotation = bottomAligned ? 270 : 0 - }; - - completeDisplayLoading(display); - } - - private void createColour(Anchor anchor) - { - bool bottomAligned = (anchor & Anchor.y2) > 0; - - var display = new ColourHitErrorMeter(hitWindows) - { - Margin = new MarginPadding(margin), - Anchor = anchor, - Origin = bottomAligned ? Anchor.CentreLeft : anchor, - Alpha = 0, - Rotation = bottomAligned ? 270 : 0 - }; - - completeDisplayLoading(display); - } - - private void completeDisplayLoading(HitErrorMeter display) - { - Add(display); - display.FadeInFromZero(fade_duration, Easing.OutQuint); - } - } -} diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 0e147f9238..5d0263772d 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -20,8 +20,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters { public class BarHitErrorMeter : HitErrorMeter { - private readonly Anchor alignment; - private const int arrow_move_duration = 400; private const int judgement_line_width = 6; @@ -43,11 +41,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters private double maxHitWindow; - public BarHitErrorMeter(HitWindows hitWindows, bool rightAligned = false) - : base(hitWindows) + public BarHitErrorMeter() { - alignment = rightAligned ? Anchor.x0 : Anchor.x2; - AutoSizeAxes = Axes.Both; } @@ -63,33 +58,42 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters Margin = new MarginPadding(2), Children = new Drawable[] { - judgementsContainer = new Container + new Container { - Anchor = Anchor.y1 | alignment, - Origin = Anchor.y1 | alignment, - Width = judgement_line_width, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = chevron_size, RelativeSizeAxes = Axes.Y, + Child = arrow = new SpriteIcon + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Y, + Y = 0.5f, + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(chevron_size), + } }, colourBars = new Container { Width = bar_width, RelativeSizeAxes = Axes.Y, - Anchor = Anchor.y1 | alignment, - Origin = Anchor.y1 | alignment, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Children = new Drawable[] { colourBarsEarly = new Container { - Anchor = Anchor.y1 | alignment, - Origin = alignment, + Anchor = Anchor.CentreLeft, + Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, Height = 0.5f, Scale = new Vector2(1, -1), }, colourBarsLate = new Container { - Anchor = Anchor.y1 | alignment, - Origin = alignment, + Anchor = Anchor.CentreLeft, + Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, Height = 0.5f, }, @@ -115,21 +119,12 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters } } }, - new Container + judgementsContainer = new Container { - Anchor = Anchor.y1 | alignment, - Origin = Anchor.y1 | alignment, - Width = chevron_size, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = judgement_line_width, RelativeSizeAxes = Axes.Y, - Child = arrow = new SpriteIcon - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Y, - Y = 0.5f, - Icon = alignment == Anchor.x2 ? FontAwesome.Solid.ChevronRight : FontAwesome.Solid.ChevronLeft, - Size = new Vector2(chevron_size), - } }, } }; @@ -152,19 +147,22 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters { var windows = HitWindows.GetAllAvailableWindows().ToArray(); - maxHitWindow = windows.First().length; + // max to avoid div-by-zero. + maxHitWindow = Math.Max(1, windows.First().length); for (var i = 0; i < windows.Length; i++) { var (result, length) = windows[i]; - colourBarsEarly.Add(createColourBar(result, (float)(length / maxHitWindow), i == 0)); - colourBarsLate.Add(createColourBar(result, (float)(length / maxHitWindow), i == 0)); + var hitWindow = (float)(length / maxHitWindow); + + colourBarsEarly.Add(createColourBar(result, hitWindow, i == 0)); + colourBarsLate.Add(createColourBar(result, hitWindow, i == 0)); } // a little nub to mark the centre point. var centre = createColourBar(windows.Last().result, 0.01f); - centre.Anchor = centre.Origin = Anchor.y1 | (alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2); + centre.Anchor = centre.Origin = Anchor.CentreLeft; centre.Width = 2.5f; colourBars.Add(centre); @@ -236,8 +234,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters judgementsContainer.Add(new JudgementLine { Y = getRelativeJudgementPosition(judgement.TimeOffset), - Anchor = alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2, - Origin = Anchor.y1 | (alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2), + Origin = Anchor.CentreLeft, }); arrow.MoveToY( diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs index 465439cf19..0eb2367f73 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; using osuTK; using osuTK.Graphics; @@ -19,8 +18,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters private readonly JudgementFlow judgementsFlow; - public ColourHitErrorMeter(HitWindows hitWindows) - : base(hitWindows) + public ColourHitErrorMeter() { AutoSizeAxes = Axes.Both; InternalChild = judgementsFlow = new JudgementFlow(); diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs index 37e9ea43c5..b0f9928b13 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs @@ -6,13 +6,15 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD.HitErrorMeters { - public abstract class HitErrorMeter : CompositeDrawable + public abstract class HitErrorMeter : CompositeDrawable, ISkinnableDrawable { - protected readonly HitWindows HitWindows; + protected HitWindows HitWindows { get; private set; } [Resolved] private ScoreProcessor processor { get; set; } @@ -20,9 +22,10 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters [Resolved] private OsuColour colours { get; set; } - protected HitErrorMeter(HitWindows hitWindows) + [BackgroundDependencyLoader(true)] + private void load(DrawableRuleset drawableRuleset) { - HitWindows = hitWindows; + HitWindows = drawableRuleset?.FirstAvailableHitWindows ?? HitWindows.Empty; } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index dcee64ff0d..ab5b01cab6 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -87,22 +87,10 @@ namespace osu.Game.Screens.Play visibilityContainer = new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Child = mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents) { - mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents) - { - RelativeSizeAxes = Axes.Both, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // still need to be migrated; a bit more involved. - new HitErrorDisplay(this.drawableRuleset?.FirstAvailableHitWindows), - } - }, - } + RelativeSizeAxes = Axes.Both, + }, }, topRightElements = new FillFlowContainer { diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index b7939b5e75..cab44c7473 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -20,10 +20,14 @@ namespace osu.Game.Screens.Play { public class SongProgress : OverlayContainer, ISkinnableDrawable { - private const int info_height = 20; - private const int bottom_bar_height = 5; + public const float MAX_HEIGHT = info_height + bottom_bar_height + graph_height + handle_height; + + private const float info_height = 20; + private const float bottom_bar_height = 5; private const float graph_height = SquareGraph.Column.WIDTH * 6; - private static readonly Vector2 handle_size = new Vector2(10, 18); + private const float handle_height = 18; + + private static readonly Vector2 handle_size = new Vector2(10, handle_height); private const float transition_duration = 200; diff --git a/osu.Game/Skinning/BeatmapSkin.cs b/osu.Game/Skinning/BeatmapSkin.cs new file mode 100644 index 0000000000..14b845faeb --- /dev/null +++ b/osu.Game/Skinning/BeatmapSkin.cs @@ -0,0 +1,32 @@ +// 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.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; +using osu.Game.Audio; +using osu.Game.Beatmaps; + +namespace osu.Game.Skinning +{ + /// + /// An empty implementation of a beatmap skin, serves as a temporary default for s. + /// + /// + /// This should be removed once becomes instantiable or a new skin type for osu!lazer beatmaps is defined. + /// + public class BeatmapSkin : Skin + { + public BeatmapSkin(BeatmapInfo beatmap) + : base(BeatmapSkinExtensions.CreateSkinInfo(beatmap), null) + { + } + + public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; + + public override IBindable GetConfig(TLookup lookup) => null; + + public override ISample GetSample(ISampleInfo sampleInfo) => null; + } +} diff --git a/osu.Game/Skinning/BeatmapSkinExtensions.cs b/osu.Game/Skinning/BeatmapSkinExtensions.cs new file mode 100644 index 0000000000..18ef09c392 --- /dev/null +++ b/osu.Game/Skinning/BeatmapSkinExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; + +namespace osu.Game.Skinning +{ + public static class BeatmapSkinExtensions + { + public static SkinInfo CreateSkinInfo(BeatmapInfo beatmap) => new SkinInfo + { + Name = beatmap.ToString(), + Creator = beatmap.Metadata?.AuthorString, + }; + } +} diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index d13ddcf22b..ba31816a07 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -14,6 +14,7 @@ using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.HitErrorMeters; using osuTK; using osuTK.Graphics; @@ -78,6 +79,24 @@ namespace osu.Game.Skinning combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5); combo.Anchor = Anchor.TopCentre; } + + var hitError = container.OfType().FirstOrDefault(); + + if (hitError != null) + { + hitError.Anchor = Anchor.CentreLeft; + hitError.Origin = Anchor.CentreLeft; + } + + var hitError2 = container.OfType().LastOrDefault(); + + if (hitError2 != null) + { + hitError2.Anchor = Anchor.CentreRight; + hitError2.Scale = new Vector2(-1, 1); + // origin flipped to match scale above. + hitError2.Origin = Anchor.CentreLeft; + } } }) { @@ -88,6 +107,8 @@ namespace osu.Game.Skinning GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)), GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)), GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.SongProgress)), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.BarHitErrorMeter)), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.BarHitErrorMeter)), } }; @@ -114,6 +135,12 @@ namespace osu.Game.Skinning case HUDSkinComponents.SongProgress: return new SongProgress(); + + case HUDSkinComponents.BarHitErrorMeter: + return new BarHitErrorMeter(); + + case HUDSkinComponents.ColourHitErrorMeter: + return new ColourHitErrorMeter(); } break; diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index 2e6c3a9937..ea39c98635 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -10,5 +10,7 @@ namespace osu.Game.Skinning AccuracyCounter, HealthDisplay, SongProgress, + BarHitErrorMeter, + ColourHitErrorMeter, } } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 91b70395f4..9ff2238e4e 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -18,7 +18,7 @@ namespace osu.Game.Skinning protected override bool UseCustomSampleBanks => true; public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, IStorageResourceProvider resources) - : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), resources, beatmap.Path) + : base(BeatmapSkinExtensions.CreateSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), resources, beatmap.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; @@ -26,12 +26,18 @@ namespace osu.Game.Skinning public override Drawable GetDrawableComponent(ISkinComponent component) { - if (component is SkinnableTargetComponent targetComponent && targetComponent.Target == SkinnableTarget.MainHUDComponents) + if (component is SkinnableTargetComponent targetComponent) { - // for now, if the beatmap skin doesn't skin the score font, fall back to current skin - // instead of potentially returning default lazer skin HUD components from here. - if (!this.HasFont(LegacyFont.Score)) - return null; + switch (targetComponent.Target) + { + case SkinnableTarget.MainHUDComponents: + // this should exist in LegacySkin instead, but there isn't a fallback skin for LegacySkins yet. + // therefore keep the check here until fallback default legacy skin is supported. + if (!this.HasFont(LegacyFont.Score)) + return null; + + break; + } } return base.GetDrawableComponent(component); @@ -63,8 +69,5 @@ namespace osu.Game.Skinning return base.GetSample(sampleInfo); } - - private static SkinInfo createSkinInfo(BeatmapInfo beatmap) => - new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata.Author.ToString() }; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 0a7b5ccc8c..1374c4c657 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -19,6 +19,7 @@ using osu.Game.IO; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Play.HUD.HitErrorMeters; using osuTK.Graphics; namespace osu.Game.Skinning @@ -341,6 +342,20 @@ namespace osu.Game.Skinning { accuracy.Y = container.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y; } + + var songProgress = container.OfType().FirstOrDefault(); + + var hitError = container.OfType().FirstOrDefault(); + + if (hitError != null) + { + hitError.Anchor = Anchor.BottomCentre; + hitError.Origin = Anchor.CentreLeft; + hitError.Rotation = -90; + + if (songProgress != null) + hitError.Y -= SongProgress.MAX_HEIGHT; + } }) { Children = new[] @@ -351,6 +366,7 @@ namespace osu.Game.Skinning GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)) ?? new DefaultAccuracyCounter(), GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)) ?? new DefaultHealthDisplay(), GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.SongProgress)) ?? new SongProgress(), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.BarHitErrorMeter)) ?? new BarHitErrorMeter(), } };