1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 16:32:54 +08:00

Merge branch 'master' into multiplayer-spectator-screen

This commit is contained in:
smoogipoo 2021-05-11 17:33:09 +09:00
commit 10a4a5decb
121 changed files with 3006 additions and 927 deletions

View File

@ -1,28 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.EmptyFreeform.Objects; using osu.Game.Rulesets.EmptyFreeform.Objects;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.EmptyFreeform.Replays namespace osu.Game.Rulesets.EmptyFreeform.Replays
{ {
public class EmptyFreeformAutoGenerator : AutoGenerator public class EmptyFreeformAutoGenerator : AutoGenerator<EmptyFreeformReplayFrame>
{ {
protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames;
public new Beatmap<EmptyFreeformHitObject> Beatmap => (Beatmap<EmptyFreeformHitObject>)base.Beatmap; public new Beatmap<EmptyFreeformHitObject> Beatmap => (Beatmap<EmptyFreeformHitObject>)base.Beatmap;
public EmptyFreeformAutoGenerator(IBeatmap beatmap) public EmptyFreeformAutoGenerator(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay();
} }
public override Replay Generate() protected override void GenerateFrames()
{ {
Frames.Add(new EmptyFreeformReplayFrame()); Frames.Add(new EmptyFreeformReplayFrame());
@ -35,8 +29,6 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
// todo: add required inputs and extra frames. // todo: add required inputs and extra frames.
}); });
} }
return Replay;
} }
} }
} }

View File

@ -1,28 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Pippidon.Objects; using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Pippidon.Replays namespace osu.Game.Rulesets.Pippidon.Replays
{ {
public class PippidonAutoGenerator : AutoGenerator public class PippidonAutoGenerator : AutoGenerator<PippidonReplayFrame>
{ {
protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames;
public new Beatmap<PippidonHitObject> Beatmap => (Beatmap<PippidonHitObject>)base.Beatmap; public new Beatmap<PippidonHitObject> Beatmap => (Beatmap<PippidonHitObject>)base.Beatmap;
public PippidonAutoGenerator(IBeatmap beatmap) public PippidonAutoGenerator(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay();
} }
public override Replay Generate() protected override void GenerateFrames()
{ {
Frames.Add(new PippidonReplayFrame()); Frames.Add(new PippidonReplayFrame());
@ -34,8 +28,6 @@ namespace osu.Game.Rulesets.Pippidon.Replays
Position = hitObject.Position, Position = hitObject.Position,
}); });
} }
return Replay;
} }
} }
} }

View File

@ -1,28 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.EmptyScrolling.Objects; using osu.Game.Rulesets.EmptyScrolling.Objects;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.EmptyScrolling.Replays namespace osu.Game.Rulesets.EmptyScrolling.Replays
{ {
public class EmptyScrollingAutoGenerator : AutoGenerator public class EmptyScrollingAutoGenerator : AutoGenerator<EmptyScrollingReplayFrame>
{ {
protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames;
public new Beatmap<EmptyScrollingHitObject> Beatmap => (Beatmap<EmptyScrollingHitObject>)base.Beatmap; public new Beatmap<EmptyScrollingHitObject> Beatmap => (Beatmap<EmptyScrollingHitObject>)base.Beatmap;
public EmptyScrollingAutoGenerator(IBeatmap beatmap) public EmptyScrollingAutoGenerator(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay();
} }
public override Replay Generate() protected override void GenerateFrames()
{ {
Frames.Add(new EmptyScrollingReplayFrame()); Frames.Add(new EmptyScrollingReplayFrame());
@ -34,8 +28,6 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
// todo: add required inputs and extra frames. // todo: add required inputs and extra frames.
}); });
} }
return Replay;
} }
} }
} }

View File

@ -2,29 +2,23 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Pippidon.Objects; using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.UI; using osu.Game.Rulesets.Pippidon.UI;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Pippidon.Replays namespace osu.Game.Rulesets.Pippidon.Replays
{ {
public class PippidonAutoGenerator : AutoGenerator public class PippidonAutoGenerator : AutoGenerator<PippidonReplayFrame>
{ {
protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames;
public new Beatmap<PippidonHitObject> Beatmap => (Beatmap<PippidonHitObject>)base.Beatmap; public new Beatmap<PippidonHitObject> Beatmap => (Beatmap<PippidonHitObject>)base.Beatmap;
public PippidonAutoGenerator(IBeatmap beatmap) public PippidonAutoGenerator(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay();
} }
public override Replay Generate() protected override void GenerateFrames()
{ {
int currentLane = 0; int currentLane = 0;
@ -55,8 +49,6 @@ namespace osu.Game.Rulesets.Pippidon.Replays
currentLane = hitObject.Lane; currentLane = hitObject.Lane;
} }
return Replay;
} }
private void addFrame(double time, PippidonAction direction) private void addFrame(double time, PippidonAction direction)

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.427.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.510.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -5,7 +5,6 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
@ -13,26 +12,19 @@ using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Catch.Replays namespace osu.Game.Rulesets.Catch.Replays
{ {
internal class CatchAutoGenerator : AutoGenerator internal class CatchAutoGenerator : AutoGenerator<CatchReplayFrame>
{ {
public const double RELEASE_DELAY = 20;
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap; public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
public CatchAutoGenerator(IBeatmap beatmap) public CatchAutoGenerator(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay();
} }
protected Replay Replay; protected override void GenerateFrames()
private CatchReplayFrame currentFrame;
public override Replay Generate()
{ {
if (Beatmap.HitObjects.Count == 0) if (Beatmap.HitObjects.Count == 0)
return Replay; return;
// todo: add support for HT DT // todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED; const double dash_speed = Catcher.BASE_SPEED;
@ -119,15 +111,11 @@ namespace osu.Game.Rulesets.Catch.Replays
} }
} }
} }
return Replay;
} }
private void addFrame(double time, float? position = null, bool dashing = false) private void addFrame(double time, float? position = null, bool dashing = false)
{ {
var last = currentFrame; Frames.Add(new CatchReplayFrame(time, position, dashing, LastFrame));
currentFrame = new CatchReplayFrame(time, position, dashing, last);
Replay.Frames.Add(currentFrame);
} }
} }
} }

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -11,7 +10,7 @@ using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Mania.Replays namespace osu.Game.Rulesets.Mania.Replays
{ {
internal class ManiaAutoGenerator : AutoGenerator internal class ManiaAutoGenerator : AutoGenerator<ManiaReplayFrame>
{ {
public const double RELEASE_DELAY = 20; public const double RELEASE_DELAY = 20;
@ -22,8 +21,6 @@ namespace osu.Game.Rulesets.Mania.Replays
public ManiaAutoGenerator(ManiaBeatmap beatmap) public ManiaAutoGenerator(ManiaBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay();
columnActions = new ManiaAction[Beatmap.TotalColumns]; columnActions = new ManiaAction[Beatmap.TotalColumns];
var normalAction = ManiaAction.Key1; var normalAction = ManiaAction.Key1;
@ -43,12 +40,10 @@ namespace osu.Game.Rulesets.Mania.Replays
} }
} }
protected Replay Replay; protected override void GenerateFrames()
public override Replay Generate()
{ {
if (Beatmap.HitObjects.Count == 0) if (Beatmap.HitObjects.Count == 0)
return Replay; return;
var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time);
@ -70,10 +65,8 @@ namespace osu.Game.Rulesets.Mania.Replays
} }
} }
Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray())); Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray()));
} }
return Replay;
} }
private IEnumerable<IActionPoint> generateActionPoints() private IEnumerable<IActionPoint> generateActionPoints()

View File

@ -26,6 +26,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
AccentColour = Color4.Transparent AccentColour = Color4.Transparent
}; };
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
// Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
AlwaysPresent = true;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
Entry = null; Entry = null;
} }
private void onEntryInvalidated() => refreshPoints(); private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
private void refreshPoints() private void refreshPoints()
{ {

View File

@ -2,10 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Beatmaps;
@ -13,7 +11,7 @@ using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Replays namespace osu.Game.Rulesets.Taiko.Replays
{ {
public class TaikoAutoGenerator : AutoGenerator public class TaikoAutoGenerator : AutoGenerator<TaikoReplayFrame>
{ {
public new TaikoBeatmap Beatmap => (TaikoBeatmap)base.Beatmap; public new TaikoBeatmap Beatmap => (TaikoBeatmap)base.Beatmap;
@ -22,16 +20,12 @@ namespace osu.Game.Rulesets.Taiko.Replays
public TaikoAutoGenerator(IBeatmap beatmap) public TaikoAutoGenerator(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
Replay = new Replay();
} }
protected Replay Replay; protected override void GenerateFrames()
protected List<ReplayFrame> Frames => Replay.Frames;
public override Replay Generate()
{ {
if (Beatmap.HitObjects.Count == 0) if (Beatmap.HitObjects.Count == 0)
return Replay; return;
bool hitButton = true; bool hitButton = true;
@ -128,8 +122,6 @@ namespace osu.Game.Rulesets.Taiko.Replays
hitButton = !hitButton; hitButton = !hitButton;
} }
return Replay;
} }
} }
} }

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Beatmaps namespace osu.Game.Tests.Beatmaps
{ {
[TestFixture] [TestFixture]
public class BeatmapDifficultyManagerTest public class BeatmapDifficultyCacheTest
{ {
[Test] [Test]
public void TestKeyEqualsWithDifferentModInstances() public void TestKeyEqualsWithDifferentModInstances()

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
@ -278,6 +279,54 @@ namespace osu.Game.Tests.NonVisual
setTime(-100, -100); setTime(-100, -100);
} }
[Test]
public void TestReplayFramesSortStability()
{
const double repeating_time = 5000;
// add a collection of frames in shuffled order time-wise; each frame also stores its original index to check stability later.
// data is hand-picked and breaks if the unstable List<T>.Sort() is used.
// in theory this can still return a false-positive with another unstable algorithm if extremely unlucky,
// but there is no conceivable fool-proof way to prevent that anyways.
replay.Frames.AddRange(new[]
{
repeating_time,
0,
3000,
repeating_time,
repeating_time,
6000,
9000,
repeating_time,
repeating_time,
1000,
11000,
21000,
4000,
repeating_time,
repeating_time,
8000,
2000,
7000,
repeating_time,
repeating_time,
10000
}.Select((time, index) => new TestReplayFrame(time, true, index)));
replay.HasReceivedAllFrames = true;
// create a new handler with the replay for the sort to be performed.
handler = new TestInputHandler(replay);
// ensure sort stability by checking that the frames with time == repeating_time are sorted in ascending frame index order themselves.
var repeatingTimeFramesData = replay.Frames
.Cast<TestReplayFrame>()
.Where(f => f.Time == repeating_time)
.Select(f => f.FrameIndex);
Assert.That(repeatingTimeFramesData, Is.Ordered.Ascending);
}
private void setReplayFrames() private void setReplayFrames()
{ {
replay.Frames = new List<ReplayFrame> replay.Frames = new List<ReplayFrame>
@ -324,11 +373,13 @@ namespace osu.Game.Tests.NonVisual
private class TestReplayFrame : ReplayFrame private class TestReplayFrame : ReplayFrame
{ {
public readonly bool IsImportant; public readonly bool IsImportant;
public readonly int FrameIndex;
public TestReplayFrame(double time, bool isImportant = false) public TestReplayFrame(double time, bool isImportant = false, int frameIndex = 0)
: base(time) : base(time)
{ {
IsImportant = isImportant; IsImportant = isImportant;
FrameIndex = frameIndex;
} }
} }

View File

@ -1,22 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
public class TestSceneComposeSelectBox : OsuTestScene public class TestSceneComposeSelectBox : OsuManualInputManagerTestScene
{ {
private Container selectionArea; private Container selectionArea;
private SelectionBox selectionBox;
public TestSceneComposeSelectBox() [SetUp]
public void SetUp() => Schedule(() =>
{ {
SelectionBox selectionBox = null;
AddStep("create box", () =>
Child = selectionArea = new Container Child = selectionArea = new Container
{ {
Size = new Vector2(400), Size = new Vector2(400),
@ -26,6 +30,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
selectionBox = new SelectionBox selectionBox = new SelectionBox
{ {
RelativeSizeAxes = Axes.Both,
CanRotate = true, CanRotate = true,
CanScaleX = true, CanScaleX = true,
CanScaleY = true, CanScaleY = true,
@ -34,12 +40,11 @@ namespace osu.Game.Tests.Visual.Editing
OnScale = handleScale OnScale = handleScale
} }
} }
}); };
AddToggleStep("toggle rotation", state => selectionBox.CanRotate = state); InputManager.MoveMouseTo(selectionBox);
AddToggleStep("toggle x", state => selectionBox.CanScaleX = state); InputManager.ReleaseButton(MouseButton.Left);
AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); });
}
private bool handleScale(Vector2 amount, Anchor reference) private bool handleScale(Vector2 amount, Anchor reference)
{ {
@ -68,5 +73,99 @@ namespace osu.Game.Tests.Visual.Editing
selectionArea.Rotation += angle; selectionArea.Rotation += angle;
return true; return true;
} }
[Test]
public void TestRotationHandleShownOnHover()
{
SelectionBoxRotationHandle rotationHandle = null;
AddStep("retrieve rotation handle", () => rotationHandle = this.ChildrenOfType<SelectionBoxRotationHandle>().First());
AddAssert("handle hidden", () => rotationHandle.Alpha == 0);
AddStep("hover over handle", () => InputManager.MoveMouseTo(rotationHandle));
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox));
AddUntilStep("handle hidden", () => rotationHandle.Alpha == 0);
}
[Test]
public void TestRotationHandleShownOnHoveringClosestScaleHandler()
{
SelectionBoxRotationHandle rotationHandle = null;
AddStep("retrieve rotation handle", () => rotationHandle = this.ChildrenOfType<SelectionBoxRotationHandle>().First());
AddAssert("rotation handle hidden", () => rotationHandle.Alpha == 0);
AddStep("hover over closest scale handle", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == rotationHandle.Anchor));
});
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox));
AddUntilStep("handle hidden", () => rotationHandle.Alpha == 0);
}
[Test]
public void TestHoverRotationHandleFromScaleHandle()
{
SelectionBoxRotationHandle rotationHandle = null;
AddStep("retrieve rotation handle", () => rotationHandle = this.ChildrenOfType<SelectionBoxRotationHandle>().First());
AddAssert("rotation handle hidden", () => rotationHandle.Alpha == 0);
AddStep("hover over closest scale handle", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == rotationHandle.Anchor));
});
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddAssert("rotation handle not hovered", () => !rotationHandle.IsHovered);
AddStep("hover over rotation handle", () => InputManager.MoveMouseTo(rotationHandle));
AddAssert("rotation handle still shown", () => rotationHandle.Alpha == 1);
AddAssert("rotation handle hovered", () => rotationHandle.IsHovered);
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox));
AddUntilStep("handle hidden", () => rotationHandle.Alpha == 0);
}
[Test]
public void TestHoldingScaleHandleHidesCorrespondingRotationHandle()
{
SelectionBoxRotationHandle rotationHandle = null;
AddStep("retrieve rotation handle", () => rotationHandle = this.ChildrenOfType<SelectionBoxRotationHandle>().First());
AddAssert("rotation handle hidden", () => rotationHandle.Alpha == 0);
AddStep("hover over closest scale handle", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == rotationHandle.Anchor));
});
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddStep("hold scale handle", () => InputManager.PressButton(MouseButton.Left));
AddUntilStep("rotation handle hidden", () => rotationHandle.Alpha == 0);
int i;
ScheduledDelegate mouseMove = null;
AddStep("start dragging", () =>
{
i = 0;
mouseMove = Scheduler.AddDelayed(() =>
{
InputManager.MoveMouseTo(selectionBox.ScreenSpaceDrawQuad.TopLeft + Vector2.One * (5 * ++i));
}, 100, true);
});
AddAssert("rotation handle still hidden", () => rotationHandle.Alpha == 0);
AddStep("end dragging", () => mouseMove.Cancel());
AddAssert("rotation handle still hidden", () => rotationHandle.Alpha == 0);
AddStep("unhold left", () => InputManager.ReleaseButton(MouseButton.Left));
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox, new Vector2(20)));
AddUntilStep("rotation handle hidden", () => rotationHandle.Alpha == 0);
}
} }
} }

View File

@ -132,8 +132,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear()); AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha == 0); AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<SelectionBox>().First().Alpha == 0);
AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha == 0); AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<SelectionBox>().First().Alpha == 0);
} }
AddStep("paste hitobject", () => Editor.Paste()); AddStep("paste hitobject", () => Editor.Paste());

View File

@ -50,9 +50,9 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{ {
new HitCircle { StartTime = 100 }, new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) }, new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(100) }, new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(150) }, new HitCircle { StartTime = 400, Position = new Vector2(300) },
})); }));
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
@ -95,9 +95,9 @@ namespace osu.Game.Tests.Visual.Editing
var addedObjects = new[] var addedObjects = new[]
{ {
new HitCircle { StartTime = 100 }, new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) }, new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(100) }, new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(150) }, new HitCircle { StartTime = 400, Position = new Vector2(300) },
}; };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
@ -131,9 +131,9 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{ {
new HitCircle { StartTime = 100 }, new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) }, new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(100) }, new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(150) }, new HitCircle { StartTime = 400, Position = new Vector2(300) },
})); }));
moveMouseToObject(() => addedObjects[0]); moveMouseToObject(() => addedObjects[0]);

View File

@ -4,9 +4,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -17,31 +19,21 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("Create combo counters", () => SetContents(() => AddStep("Create combo counters", () => SetContents(() => new SkinnableComboCounter()));
{
var comboCounter = new SkinnableComboCounter();
comboCounter.Current.Value = 1;
return comboCounter;
}));
} }
[Test] [Test]
public void TestComboCounterIncrementing() public void TestComboCounterIncrementing()
{ {
AddRepeatStep("increase combo", () => AddRepeatStep("increase combo", () => scoreProcessor.Combo.Value++, 10);
{
foreach (var counter in comboCounters)
counter.Current.Value++;
}, 10);
AddStep("reset combo", () => AddStep("reset combo", () => scoreProcessor.Combo.Value = 0);
{
foreach (var counter in comboCounters)
counter.Current.Value = 0;
});
} }
} }
} }

View File

@ -4,7 +4,8 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Testing; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -20,24 +21,28 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
[SetUpSteps] private void create(HealthProcessor healthProcessor)
public void SetUpSteps()
{ {
AddStep("create layer", () => AddStep("create layer", () =>
{ {
Child = layer = new FailingLayer(); Child = new HealthProcessorContainer(healthProcessor)
layer.BindHealthProcessor(new DrainingHealthProcessor(1)); {
RelativeSizeAxes = Axes.Both,
Child = layer = new FailingLayer()
};
layer.ShowHealth.BindTo(showHealth); layer.ShowHealth.BindTo(showHealth);
}); });
AddStep("show health", () => showHealth.Value = true); AddStep("show health", () => showHealth.Value = true);
AddStep("enable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddStep("enable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true));
AddUntilStep("layer is visible", () => layer.IsPresent);
} }
[Test] [Test]
public void TestLayerFading() public void TestLayerFading()
{ {
create(new DrainingHealthProcessor(0));
AddSliderStep("current health", 0.0, 1.0, 1.0, val => AddSliderStep("current health", 0.0, 1.0, 1.0, val =>
{ {
if (layer != null) if (layer != null)
@ -53,6 +58,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestLayerDisabledViaConfig() public void TestLayerDisabledViaConfig()
{ {
create(new DrainingHealthProcessor(0));
AddUntilStep("layer is visible", () => layer.IsPresent);
AddStep("disable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false)); AddStep("disable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
AddStep("set health to 0.10", () => layer.Current.Value = 0.1); AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
AddUntilStep("layer is not visible", () => !layer.IsPresent); AddUntilStep("layer is not visible", () => !layer.IsPresent);
@ -61,7 +68,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestLayerVisibilityWithAccumulatingProcessor() public void TestLayerVisibilityWithAccumulatingProcessor()
{ {
AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new AccumulatingHealthProcessor(1))); create(new AccumulatingHealthProcessor(1));
AddUntilStep("layer is not visible", () => !layer.IsPresent);
AddStep("set health to 0.10", () => layer.Current.Value = 0.1); AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
AddUntilStep("layer is not visible", () => !layer.IsPresent); AddUntilStep("layer is not visible", () => !layer.IsPresent);
} }
@ -69,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestLayerVisibilityWithDrainingProcessor() public void TestLayerVisibilityWithDrainingProcessor()
{ {
AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new DrainingHealthProcessor(1))); create(new DrainingHealthProcessor(0));
AddStep("set health to 0.10", () => layer.Current.Value = 0.1); AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
AddWaitStep("wait for potential fade", 10); AddWaitStep("wait for potential fade", 10);
AddAssert("layer is still visible", () => layer.IsPresent); AddAssert("layer is still visible", () => layer.IsPresent);
@ -78,6 +86,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestLayerVisibilityWithDifferentOptions() public void TestLayerVisibilityWithDifferentOptions()
{ {
create(new DrainingHealthProcessor(0));
AddStep("set health to 0.10", () => layer.Current.Value = 0.1); AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
AddStep("don't show health", () => showHealth.Value = false); AddStep("don't show health", () => showHealth.Value = false);
@ -96,5 +106,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("enable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddStep("enable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true));
AddUntilStep("layer fade is visible", () => layer.IsPresent); AddUntilStep("layer fade is visible", () => layer.IsPresent);
} }
private class HealthProcessorContainer : Container
{
[Cached(typeof(HealthProcessor))]
private readonly HealthProcessor healthProcessor;
public HealthProcessorContainer(HealthProcessor healthProcessor)
{
this.healthProcessor = healthProcessor;
}
}
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK.Input; using osuTK.Input;
@ -19,6 +20,12 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
@ -31,9 +38,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
createNew(); createNew();
AddRepeatStep("increase combo", () => { hudOverlay.ComboCounter.Current.Value++; }, 10); AddRepeatStep("increase combo", () => { scoreProcessor.Combo.Value++; }, 10);
AddStep("reset combo", () => { hudOverlay.ComboCounter.Current.Value = 0; }); AddStep("reset combo", () => { scoreProcessor.Combo.Value = 0; });
} }
[Test] [Test]
@ -139,12 +146,12 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("create overlay", () => AddStep("create overlay", () =>
{ {
hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>()); hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
hudOverlay.ComboCounter.Current.Value = 1; scoreProcessor.Combo.Value = 1;
action?.Invoke(hudOverlay); action?.Invoke(hudOverlay);

View File

@ -2,15 +2,16 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Rulesets.Judgements; using osu.Framework.Allocation;
using osu.Framework.Utils;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.Taiko.Scoring;
@ -20,14 +21,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneHitErrorMeter : OsuTestScene public class TestSceneHitErrorMeter : OsuTestScene
{ {
private BarHitErrorMeter barMeter;
private BarHitErrorMeter barMeter2;
private BarHitErrorMeter barMeter3;
private ColourHitErrorMeter colourMeter;
private ColourHitErrorMeter colourMeter2;
private ColourHitErrorMeter colourMeter3;
private HitWindows hitWindows; private HitWindows hitWindows;
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
public TestSceneHitErrorMeter() public TestSceneHitErrorMeter()
{ {
recreateDisplay(new OsuHitWindows(), 5); recreateDisplay(new OsuHitWindows(), 5);
@ -105,40 +103,40 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
}); });
Add(barMeter = new BarHitErrorMeter(hitWindows, true) Add(new BarHitErrorMeter(hitWindows, true)
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
}); });
Add(barMeter2 = new BarHitErrorMeter(hitWindows, false) Add(new BarHitErrorMeter(hitWindows, false)
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
}); });
Add(barMeter3 = new BarHitErrorMeter(hitWindows, true) Add(new BarHitErrorMeter(hitWindows, true)
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Rotation = 270, Rotation = 270,
}); });
Add(colourMeter = new ColourHitErrorMeter(hitWindows) Add(new ColourHitErrorMeter(hitWindows)
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 50 } Margin = new MarginPadding { Right = 50 }
}); });
Add(colourMeter2 = new ColourHitErrorMeter(hitWindows) Add(new ColourHitErrorMeter(hitWindows)
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 } Margin = new MarginPadding { Left = 50 }
}); });
Add(colourMeter3 = new ColourHitErrorMeter(hitWindows) Add(new ColourHitErrorMeter(hitWindows)
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
@ -149,18 +147,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private void newJudgement(double offset = 0) private void newJudgement(double offset = 0)
{ {
var judgement = new JudgementResult(new HitObject(), new Judgement()) scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
{ {
TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset, TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset,
Type = HitResult.Perfect, Type = HitResult.Perfect,
}; });
barMeter.OnNewJudgement(judgement);
barMeter2.OnNewJudgement(judgement);
barMeter3.OnNewJudgement(judgement);
colourMeter.OnNewJudgement(judgement);
colourMeter2.OnNewJudgement(judgement);
colourMeter3.OnNewJudgement(judgement);
} }
} }
} }

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinEditor : PlayerTestScene
{
private SkinEditor skinEditor;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add editor overlay", () =>
{
skinEditor?.Expire();
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
});
}
[Test]
public void TestToggleEditor()
{
AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility());
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
}
}

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning.Editor;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
{
[Cached]
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor();
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create editor overlay", () =>
{
SetContents(() =>
{
var ruleset = new OsuRuleset();
var working = CreateWorkingBeatmap(ruleset.RulesetInfo);
var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo);
var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap);
var hudOverlay = new HUDOverlay(drawableRuleset, Array.Empty<Mod>())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
// Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
scoreProcessor.Combo.Value = 1;
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
drawableRuleset,
hudOverlay,
new SkinEditor(hudOverlay),
}
};
});
});
}
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
}
}

View File

@ -1,49 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneSkinnableAccuracyCounter : SkinnableTestScene public class TestSceneSkinnableAccuracyCounter : SkinnableTestScene
{ {
private IEnumerable<SkinnableAccuracyCounter> accuracyCounters => CreatedDrawables.OfType<SkinnableAccuracyCounter>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("Create combo counters", () => SetContents(() => AddStep("Set initial accuracy", () => scoreProcessor.Accuracy.Value = 1);
{ AddStep("Create accuracy counters", () => SetContents(() => new SkinnableAccuracyCounter()));
var accuracyCounter = new SkinnableAccuracyCounter();
accuracyCounter.Current.Value = 1;
return accuracyCounter;
}));
} }
[Test] [Test]
public void TestChangingAccuracy() public void TestChangingAccuracy()
{ {
AddStep(@"Reset all", delegate AddStep(@"Reset all", () => scoreProcessor.Accuracy.Value = 1);
{
foreach (var s in accuracyCounters)
s.Current.Value = 1;
});
AddStep(@"Hit! :D", delegate AddStep(@"Miss :(", () => scoreProcessor.Accuracy.Value -= 0.023);
{
foreach (var s in accuracyCounters)
s.Current.Value -= 0.023f;
});
} }
} }
} }

View File

@ -14,6 +14,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK.Input; using osuTK.Input;
@ -23,6 +24,12 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>(); private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
// best way to check without exposing. // best way to check without exposing.
@ -37,17 +44,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
createNew(); createNew();
AddRepeatStep("increase combo", () => AddRepeatStep("increase combo", () => scoreProcessor.Combo.Value++, 10);
{
foreach (var hud in hudOverlays)
hud.ComboCounter.Current.Value++;
}, 10);
AddStep("reset combo", () => AddStep("reset combo", () => scoreProcessor.Combo.Value = 0);
{
foreach (var hud in hudOverlays)
hud.ComboCounter.Current.Value = 0;
});
} }
[Test] [Test]
@ -80,13 +79,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
SetContents(() => SetContents(() =>
{ {
hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>()); hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually. // Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
hudOverlay.ComboCounter.Current.Value = 1;
action?.Invoke(hudOverlay); action?.Invoke(hudOverlay);
return hudOverlay; return hudOverlay;

View File

@ -4,11 +4,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -19,6 +22,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
@ -28,8 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
AddStep(@"Reset all", delegate AddStep(@"Reset all", delegate
{ {
foreach (var s in healthDisplays) healthProcessor.Health.Value = 1;
s.Current.Value = 1;
}); });
} }
@ -38,23 +43,21 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddRepeatStep(@"decrease hp", delegate AddRepeatStep(@"decrease hp", delegate
{ {
foreach (var healthDisplay in healthDisplays) healthProcessor.Health.Value -= 0.08f;
healthDisplay.Current.Value -= 0.08f;
}, 10); }, 10);
AddRepeatStep(@"increase hp without flash", delegate AddRepeatStep(@"increase hp without flash", delegate
{ {
foreach (var healthDisplay in healthDisplays) healthProcessor.Health.Value += 0.1f;
healthDisplay.Current.Value += 0.1f;
}, 3); }, 3);
AddRepeatStep(@"increase hp with flash", delegate AddRepeatStep(@"increase hp with flash", delegate
{ {
foreach (var healthDisplay in healthDisplays) healthProcessor.Health.Value += 0.1f;
healthProcessor.ApplyResult(new JudgementResult(new HitCircle(), new OsuJudgement())
{ {
healthDisplay.Current.Value += 0.1f; Type = HitResult.Perfect
healthDisplay.Flash(new JudgementResult(null, new OsuJudgement())); });
}
}, 3); }, 3);
} }
} }

View File

@ -4,10 +4,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -18,37 +20,27 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("Create combo counters", () => SetContents(() => AddStep("Create score counters", () => SetContents(() => new SkinnableScoreCounter()));
{
var comboCounter = new SkinnableScoreCounter();
comboCounter.Current.Value = 1;
return comboCounter;
}));
} }
[Test] [Test]
public void TestScoreCounterIncrementing() public void TestScoreCounterIncrementing()
{ {
AddStep(@"Reset all", delegate AddStep(@"Reset all", () => scoreProcessor.TotalScore.Value = 0);
{
foreach (var s in scoreCounters)
s.Current.Value = 0;
});
AddStep(@"Hit! :D", delegate AddStep(@"Hit! :D", () => scoreProcessor.TotalScore.Value += 300);
{
foreach (var s in scoreCounters)
s.Current.Value += 300;
});
} }
[Test] [Test]
public void TestVeryLargeScore() public void TestVeryLargeScore()
{ {
AddStep("set large score", () => scoreCounters.ForEach(counter => counter.Current.Value = 1_000_000_000)); AddStep("set large score", () => scoreCounters.ForEach(counter => scoreProcessor.TotalScore.Value = 1_000_000_000));
} }
} }
} }

View File

@ -0,0 +1,201 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneStoryboardWithOutro : PlayerTestScene
{
protected override bool HasCustomSteps => true;
protected new OutroPlayer Player => (OutroPlayer)base.Player;
private double currentStoryboardDuration;
private bool showResults = true;
private event Func<HealthProcessor, JudgementResult, bool> currentFailConditions;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
AddStep("reset fail conditions", () => currentFailConditions = (_, __) => false);
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
AddStep("set ShowResults = true", () => showResults = true);
}
[Test]
public void TestStoryboardSkipOutro()
{
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardNoSkipOutro()
{
CreateTest(null);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardExitToSkipOutro()
{
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("score shown", () => Player.IsScoreShown);
}
[TestCase(false)]
[TestCase(true)]
public void TestStoryboardToggle(bool enabledAtBeginning)
{
CreateTest(null);
AddStep($"{(enabledAtBeginning ? "enable" : "disable")} storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, enabledAtBeginning));
AddStep("toggle storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, !enabledAtBeginning));
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestOutroEndsDuringFailAnimation()
{
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
});
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
}
[Test]
public void TestShowResultsFalse()
{
CreateTest(() =>
{
AddStep("set ShowResults = false", () => showResults = false);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddWaitStep("wait", 10);
AddAssert("no score shown", () => !Player.IsScoreShown);
}
[Test]
public void TestStoryboardEndsBeforeCompletion()
{
CreateTest(() => AddStep("set storyboard duration to .1s", () => currentStoryboardDuration = 100));
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardRewind()
{
SkipOverlay.FadeContainer fadeContainer() => Player.ChildrenOfType<SkipOverlay.FadeContainer>().First();
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-1000));
AddUntilStep("skip overlay content not visible", () => fadeContainer().State == Visibility.Hidden);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
}
[Test]
public void TestPerformExitNoOutro()
{
CreateTest(null);
AddStep("disable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, false));
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("player exited", () => Stack.CurrentScreen == null);
}
protected override bool AllowFail => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OutroPlayer(currentFailConditions, showResults);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new HitCircle());
return beatmap;
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
return base.CreateWorkingBeatmap(beatmap, createStoryboard(currentStoryboardDuration));
}
private Storyboard createStoryboard(double duration)
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
}
protected class OutroPlayer : TestPlayer
{
public void ExitViaPause() => PerformExit(true);
public new FailOverlay FailOverlay => base.FailOverlay;
public bool IsScoreShown => !this.IsCurrentScreen() && this.GetChildScreen() is ResultsScreen;
private event Func<HealthProcessor, JudgementResult, bool> failConditions;
public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true)
: base(false, showResults)
{
this.failConditions = failConditions;
}
protected override void LoadComplete()
{
base.LoadComplete();
HealthProcessor.FailConditions += failConditions;
}
protected override Task ImportScore(Score score)
{
return Task.CompletedTask;
}
}
}
}

View File

@ -2,7 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -14,21 +17,34 @@ namespace osu.Game.Tests.Visual.Online
{ {
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private NewsOverlay news; private NewsOverlay overlay;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => Child = news = new NewsOverlay()); public void SetUp() => Schedule(() => Child = overlay = new NewsOverlay());
[Test] [Test]
public void TestRequest() public void TestRequest()
{ {
setUpNewsResponse(responseExample); setUpNewsResponse(responseExample);
AddStep("Show", () => news.Show()); AddStep("Show", () => overlay.Show());
AddStep("Show article", () => news.ShowArticle("article")); AddStep("Show article", () => overlay.ShowArticle("article"));
} }
private void setUpNewsResponse(GetNewsResponse r) [Test]
=> AddStep("set up response", () => public void TestCursorRequest()
{
setUpNewsResponse(responseWithCursor, "Set up cursor response");
AddStep("Show", () => overlay.Show());
AddUntilStep("Show More button is visible", () => showMoreButton?.Alpha == 1);
setUpNewsResponse(responseWithNoCursor, "Set up no cursor response");
AddStep("Click Show More", () => showMoreButton?.Click());
AddUntilStep("Show More button is hidden", () => showMoreButton?.Alpha == 0);
}
private ShowMoreButton showMoreButton => overlay.ChildrenOfType<ShowMoreButton>().FirstOrDefault();
private void setUpNewsResponse(GetNewsResponse r, string testName = "Set up response")
=> AddStep(testName, () =>
{ {
dummyAPI.HandleRequest = request => dummyAPI.HandleRequest = request =>
{ {
@ -40,7 +56,7 @@ namespace osu.Game.Tests.Visual.Online
}; };
}); });
private GetNewsResponse responseExample => new GetNewsResponse private static GetNewsResponse responseExample => new GetNewsResponse
{ {
NewsPosts = new[] NewsPosts = new[]
{ {
@ -62,5 +78,37 @@ namespace osu.Game.Tests.Visual.Online
} }
} }
}; };
private static GetNewsResponse responseWithCursor => new GetNewsResponse
{
NewsPosts = new[]
{
new APINewsPost
{
Title = "This post has an image which starts with \"/\" and has many authors!",
Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
Author = "someone, someone1, someone2, someone3, someone4",
FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png",
PublishedAt = DateTimeOffset.Now
}
},
Cursor = new Cursor()
};
private static GetNewsResponse responseWithNoCursor => new GetNewsResponse
{
NewsPosts = new[]
{
new APINewsPost
{
Title = "This post has a full-url image! (HTML entity: &amp;)",
Preview = "boom (HTML entity: &amp;)",
Author = "user (HTML entity: &amp;)",
FirstImage = "https://assets.ppy.sh/artists/88/header.jpg",
PublishedAt = DateTimeOffset.Now
}
},
Cursor = null
};
} }
} }

View File

@ -7,6 +7,7 @@ using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -107,13 +108,13 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version"); AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version");
AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title"); AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title");
AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist");
AddAssert("check author", () => infoWedge.Info.MapperContainer.Children.OfType<OsuSpriteText>().Any(s => s.Current.Value == $"{ruleset.ShortName}Author")); AddAssert("check author", () => infoWedge.Info.MapperContainer.ChildrenOfType<OsuSpriteText>().Any(s => s.Current.Value == $"{ruleset.ShortName}Author"));
} }
private void testInfoLabels(int expectedCount) private void testInfoLabels(int expectedCount)
{ {
AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any()); AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Count() == expectedCount); AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
} }
[Test] [Test]
@ -123,8 +124,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Current.Value)); AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Current.Value));
AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any()); AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.ChildrenOfType<OsuSpriteText>().Any());
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any()); AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
} }
[Test] [Test]
@ -135,15 +136,15 @@ namespace osu.Game.Tests.Visual.SongSelect
private void selectBeatmap([CanBeNull] IBeatmap b) private void selectBeatmap([CanBeNull] IBeatmap b)
{ {
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null; Container containerBefore = null;
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{ {
infoBefore = infoWedge.Info; containerBefore = infoWedge.DisplayedContent;
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
}); });
AddUntilStep("wait for async load", () => infoWedge.Info != infoBefore); AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
} }
private IBeatmap createTestBeatmap(RulesetInfo ruleset) private IBeatmap createTestBeatmap(RulesetInfo ruleset)
@ -193,7 +194,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private class TestBeatmapInfoWedge : BeatmapInfoWedge private class TestBeatmapInfoWedge : BeatmapInfoWedge
{ {
public new BufferedWedgeInfo Info => base.Info; public new Container DisplayedContent => base.DisplayedContent;
public new WedgeInfoText Info => base.Info;
} }
private class TestHitObject : ConvertHitObject, IHasPosition private class TestHitObject : ConvertHitObject, IHasPosition

View File

@ -304,6 +304,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep(@"Sort by BPM", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM)); AddStep(@"Sort by BPM", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM));
AddStep(@"Sort by Length", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length)); AddStep(@"Sort by Length", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length));
AddStep(@"Sort by Difficulty", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty)); AddStep(@"Sort by Difficulty", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty));
AddStep(@"Sort by Source", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source));
} }
[Test] [Test]

View File

@ -0,0 +1,240 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneOsuMarkdownContainer : OsuTestScene
{
private OsuMarkdownContainer markdownContainer;
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Orange);
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[]
{
new Box
{
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = markdownContainer = new OsuMarkdownContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
};
});
[Test]
public void TestEmphases()
{
AddStep("Emphases", () =>
{
markdownContainer.Text = @"_italic with underscore_
*italic with asterisk*
__bold with underscore__
**bold with asterisk**
*__italic with asterisk, bold with underscore__*
_**italic with underscore, bold with asterisk**_";
});
}
[Test]
public void TestHeading()
{
AddStep("Add Heading", () =>
{
markdownContainer.Text = @"# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5";
});
}
[Test]
public void TestLink()
{
AddStep("Add Link", () =>
{
markdownContainer.Text = "[Welcome to osu!](https://osu.ppy.sh)";
});
}
[Test]
public void TestLinkWithInlineText()
{
AddStep("Add Link with inline text", () =>
{
markdownContainer.Text = "Hey, [welcome to osu!](https://osu.ppy.sh) Please enjoy the game.";
});
}
[Test]
public void TestInlineCode()
{
AddStep("Add inline code", () =>
{
markdownContainer.Text = "This is `inline code` text";
});
}
[Test]
public void TestParagraph()
{
AddStep("Add paragraph", () =>
{
markdownContainer.Text = @"first paragraph
second paragraph
third paragraph";
});
}
[Test]
public void TestFencedCodeBlock()
{
AddStep("Add Code Block", () =>
{
markdownContainer.Text = @"```markdown
# Markdown code block
This is markdown code block.
```";
});
}
[Test]
public void TestSeparator()
{
AddStep("Add Seperator", () =>
{
markdownContainer.Text = @"Line above
---
Line below";
});
}
[Test]
public void TestQuote()
{
AddStep("Add quote", () =>
{
markdownContainer.Text =
@"> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.";
});
}
[Test]
public void TestTable()
{
AddStep("Add Table", () =>
{
markdownContainer.Text =
@"| Left Aligned | Center Aligned | Right Aligned |
| :------------------- | :--------------------: | ---------------------:|
| Long Align Left Text | Long Align Center Text | Long Align Right Text |
| Align Left | Align Center | Align Right |
| Left | Center | Right |";
});
}
[Test]
public void TestUnorderedList()
{
AddStep("Add Unordered List", () =>
{
markdownContainer.Text = @"- First item level 1
- Second item level 1
- First item level 2
- First item level 3
- Second item level 3
- Third item level 3
- First item level 4
- Second item level 4
- Third item level 4
- Second item level 2
- Third item level 2
- Third item level 1";
});
}
[Test]
public void TestOrderedList()
{
AddStep("Add Ordered List", () =>
{
markdownContainer.Text = @"1. First item level 1
2. Second item level 1
1. First item level 2
1. First item level 3
2. Second item level 3
3. Third item level 3
1. First item level 4
2. Second item level 4
3. Third item level 4
2. Second item level 2
3. Third item level 2
3. Third item level 1";
});
}
[Test]
public void TestLongMixedList()
{
AddStep("Add long mixed list", () =>
{
markdownContainer.Text = @"1. The osu! World Cup is a country-based team tournament played on the osu! game mode.
- While this competition is planned as a 4 versus 4 setup, this may change depending on the number of incoming registrations.
2. Beatmap scoring is based on Score V2.
3. The beatmaps for each round will be announced by the map selectors in advance on the Sunday before the actual matches take place. Only these beatmaps will be used during the respective matches.
- One beatmap will be a tiebreaker beatmap. This beatmap will only be played in case of a tie. **The only exception to this is the Qualifiers pool.**
4. The match schedule will be settled by the Tournament Management (see the [scheduling instructions](#scheduling-instructions)).
5. If no staff or referee is available, the match will be postponed.
6. Use of the Visual Settings to alter background dim or disable beatmap elements like storyboards and skins are allowed.
7. If the beatmap ends in a draw, the map will be nullified and replayed.
8. If a player disconnects, their scores will not be counted towards their team's total.
- Disconnects within 30 seconds or 25% of the beatmap length (whichever happens first) after beatmap begin can be aborted and/or rematched. This is up to the referee's discretion.
9. Beatmaps cannot be reused in the same match unless the map was nullified.
10. If less than the minimum required players attend, the maximum time the match can be postponed is 10 minutes.
11. Exchanging players during a match is allowed without limitations.
- **If a map rematch is required, exchanging players is not allowed. With the referee's discretion, an exception can be made if the previous roster is unavailable to play.**
12. Lag is not a valid reason to nullify a beatmap.
13. All players are supposed to keep the match running fluently and without delays. Penalties can be issued to the players if they cause excessive match delays.
14. If a player disconnects between maps and the team cannot provide a replacement, the match can be delayed 10 minutes at maximum.
15. All players and referees must be treated with respect. Instructions of the referees and tournament Management are to be followed. Decisions labeled as final are not to be objected.
16. Disrupting the match by foul play, insulting and provoking other players or referees, delaying the match or other deliberate inappropriate misbehavior is strictly prohibited.
17. The multiplayer chatrooms are subject to the [osu! community rules](/wiki/Rules).
- Breaking the chat rules will result in a silence. Silenced players can not participate in multiplayer matches and must be exchanged for the time being.
18. **The seeding method will be revealed after all the teams have played their Qualifier rounds.**
19. Unexpected incidents are handled by the tournament management. Referees may allow higher tolerance depending on the circumstances. This is up to their discretion.
20. Penalties for violating the tournament rules may include:
- Exclusion of specific players for one beatmap
- Exclusion of specific players for an entire match
- Declaring the match as Lost by Default
- Disqualification from the entire tournament
- Disqualification from the current and future official tournaments until appealed
- Any modification of these rules will be announced.";
});
}
}
}

View File

@ -0,0 +1,82 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig;
using Markdig.Extensions.AutoIdentifiers;
using Markdig.Extensions.Tables;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownContainer : MarkdownContainer
{
public OsuMarkdownContainer()
{
LineSpacing = 21;
}
protected override void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level)
{
switch (markdownObject)
{
case YamlFrontMatterBlock _:
// Don't parse YAML Frontmatter
break;
case ListItemBlock listItemBlock:
var isOrdered = ((ListBlock)listItemBlock.Parent).IsOrdered;
var childContainer = CreateListItem(listItemBlock, level, isOrdered);
container.Add(childContainer);
foreach (var single in listItemBlock)
base.AddMarkdownComponent(single, childContainer.Content, level);
break;
default:
base.AddMarkdownComponent(markdownObject, container, level);
break;
}
}
public override SpriteText CreateSpriteText() => new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14),
};
public override MarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer();
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new OsuMarkdownHeading(headingBlock);
protected override MarkdownFencedCodeBlock CreateFencedCodeBlock(FencedCodeBlock fencedCodeBlock) => new OsuMarkdownFencedCodeBlock(fencedCodeBlock);
protected override MarkdownSeparator CreateSeparator(ThematicBreakBlock thematicBlock) => new OsuMarkdownSeparator();
protected override MarkdownQuoteBlock CreateQuoteBlock(QuoteBlock quoteBlock) => new OsuMarkdownQuoteBlock(quoteBlock);
protected override MarkdownTable CreateTable(Table table) => new OsuMarkdownTable(table);
protected override MarkdownList CreateList(ListBlock listBlock) => new MarkdownList
{
Padding = new MarginPadding(0)
};
protected virtual OsuMarkdownListItem CreateListItem(ListItemBlock listItemBlock, int level, bool isOrdered)
{
if (isOrdered)
return new OsuMarkdownOrderedListItem(listItemBlock.Order);
return new OsuMarkdownUnorderedListItem(level);
}
protected override MarkdownPipeline CreateBuilder()
=> new MarkdownPipelineBuilder().UseAutoIdentifiers(AutoIdentifierOptions.GitHub)
.UseEmojiAndSmiley()
.UseYamlFrontMatter()
.UseAdvancedExtensions().Build();
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Syntax;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownFencedCodeBlock : MarkdownFencedCodeBlock
{
// TODO : change to monospace font for this component
public OsuMarkdownFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
: base(fencedCodeBlock)
{
}
protected override Drawable CreateBackground() => new CodeBlockBackground();
public override MarkdownTextFlowContainer CreateTextFlow() => new CodeBlockTextFlowContainer();
private class CodeBlockBackground : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
Colour = colourProvider.Background6;
}
}
private class CodeBlockTextFlowContainer : OsuMarkdownTextFlowContainer
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Light1;
Margin = new MarginPadding(10);
}
}
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Syntax;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownHeading : MarkdownHeading
{
private readonly int level;
public OsuMarkdownHeading(HeadingBlock headingBlock)
: base(headingBlock)
{
level = headingBlock.Level;
}
public override MarkdownTextFlowContainer CreateTextFlow() => new HeadingTextFlowContainer
{
Weight = GetFontWeightByLevel(level),
};
protected override float GetFontSizeByLevel(int level)
{
// Reference for this font size
// https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/bem/osu-md.less#L9
// https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/variables.less#L161
const float base_font_size = 14;
switch (level)
{
case 1:
return 30 / base_font_size;
case 2:
return 26 / base_font_size;
case 3:
return 20 / base_font_size;
case 4:
return 18 / base_font_size;
case 5:
return 16 / base_font_size;
default:
return 1;
}
}
protected virtual FontWeight GetFontWeightByLevel(int level)
{
switch (level)
{
case 1:
case 2:
return FontWeight.SemiBold;
default:
return FontWeight.Bold;
}
}
private class HeadingTextFlowContainer : OsuMarkdownTextFlowContainer
{
public FontWeight Weight { get; set; }
protected override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(weight: Weight));
}
}
}

View File

@ -0,0 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownLinkText : MarkdownLinkText
{
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
private SpriteText spriteText;
public OsuMarkdownLinkText(string text, LinkInline linkInline)
: base(text, linkInline)
{
}
[BackgroundDependencyLoader]
private void load()
{
spriteText.Colour = colourProvider.Light2;
}
public override SpriteText CreateSpriteText()
{
return spriteText = base.CreateSpriteText();
}
protected override bool OnHover(HoverEvent e)
{
spriteText.Colour = colourProvider.Light1;
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
spriteText.Colour = colourProvider.Light2;
base.OnHoverLost(e);
}
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osuTK;
namespace osu.Game.Graphics.Containers.Markdown
{
public abstract class OsuMarkdownListItem : CompositeDrawable
{
[Resolved]
private IMarkdownTextComponent parentTextComponent { get; set; }
public FillFlowContainer Content { get; private set; }
protected OsuMarkdownListItem()
{
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
CreateMarker(),
Content = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10, 10),
}
};
}
protected virtual SpriteText CreateMarker() => parentTextComponent.CreateSpriteText();
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownOrderedListItem : OsuMarkdownListItem
{
private const float left_padding = 30;
private readonly int order;
public OsuMarkdownOrderedListItem(int order)
{
this.order = order;
Padding = new MarginPadding { Left = left_padding };
}
protected override SpriteText CreateMarker() => base.CreateMarker().With(t =>
{
t.X = -left_padding;
t.Text = $"{order}.";
});
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Syntax;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownQuoteBlock : MarkdownQuoteBlock
{
public OsuMarkdownQuoteBlock(QuoteBlock quoteBlock)
: base(quoteBlock)
{
}
protected override Drawable CreateBackground() => new QuoteBackground();
public override MarkdownTextFlowContainer CreateTextFlow()
{
return base.CreateTextFlow().With(f => f.Margin = new MarginPadding
{
Vertical = 10,
Horizontal = 20,
});
}
private class QuoteBackground : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
RelativeSizeAxes = Axes.Y;
Width = 2;
Colour = colourProvider.Content2;
}
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownSeparator : MarkdownSeparator
{
protected override Drawable CreateSeparator() => new Separator();
private class Separator : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
Height = 1;
Colour = colourProvider.Background3;
}
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Extensions.Tables;
using osu.Framework.Graphics.Containers.Markdown;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownTable : MarkdownTable
{
public OsuMarkdownTable(Table table)
: base(table)
{
}
protected override MarkdownTableCell CreateTableCell(TableCell cell, TableColumnDefinition definition, bool isHeading) => new OsuMarkdownTableCell(cell, definition, isHeading);
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Extensions.Tables;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownTableCell : MarkdownTableCell
{
private readonly bool isHeading;
public OsuMarkdownTableCell(TableCell cell, TableColumnDefinition definition, bool isHeading)
: base(cell, definition)
{
this.isHeading = isHeading;
Masking = false;
BorderThickness = 0;
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(CreateBorder(isHeading));
}
public override MarkdownTextFlowContainer CreateTextFlow() => new TableCellTextFlowContainer
{
Weight = isHeading ? FontWeight.Bold : FontWeight.Regular,
Padding = new MarginPadding(10),
};
protected virtual Box CreateBorder(bool isHeading)
{
if (isHeading)
return new TableHeadBorder();
return new TableBodyBorder();
}
private class TableHeadBorder : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background3;
RelativeSizeAxes = Axes.X;
Height = 2;
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
}
}
private class TableBodyBorder : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background4;
RelativeSizeAxes = Axes.X;
Height = 1;
}
}
private class TableCellTextFlowContainer : OsuMarkdownTextFlowContainer
{
public FontWeight Weight { get; set; }
protected override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(weight: Weight));
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownTextFlowContainer : MarkdownTextFlowContainer
{
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
protected override void AddLinkText(string text, LinkInline linkInline)
=> AddDrawable(new OsuMarkdownLinkText(text, linkInline));
// TODO : Add background (colour B6) and change font to monospace
protected override void AddCodeInLine(CodeInline codeInline)
=> AddText(codeInline.Content, t => { t.Colour = colourProvider.Light1; });
protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
=> CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic));
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownUnorderedListItem : OsuMarkdownListItem
{
private const float left_padding = 20;
private readonly int level;
public OsuMarkdownUnorderedListItem(int level)
{
this.level = level;
Padding = new MarginPadding { Left = left_padding };
}
protected override SpriteText CreateMarker() => base.CreateMarker().With(t =>
{
t.Text = GetTextMarker(level);
t.Font = t.Font.With(size: t.Font.Size / 2);
t.Origin = Anchor.Centre;
t.X = -left_padding / 2;
t.Y = t.Font.Size;
});
/// <summary>
/// Get text marker based on <paramref name="level"/>
/// </summary>
/// <param name="level">The markdown level of current list item.</param>
/// <returns>The marker string of this list item</returns>
protected virtual string GetTextMarker(int level)
{
switch (level)
{
case 1:
return "●";
case 2:
return "○";
default:
return "■";
}
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Graphics.Containers
protected override bool BlockNonPositionalInput => true; protected override bool BlockNonPositionalInput => true;
/// <summary> /// <summary>
/// Temporary to allow for overlays in the main screen content to not dim theirselves. /// Temporary to allow for overlays in the main screen content to not dim themselves.
/// Should be eventually replaced by dimming which is aware of the target dim container (traverse parent for certain interface type?). /// Should be eventually replaced by dimming which is aware of the target dim container (traverse parent for certain interface type?).
/// </summary> /// </summary>
protected virtual bool DimMainContent => true; protected virtual bool DimMainContent => true;

View File

@ -36,6 +36,24 @@ namespace osu.Game.Graphics.Containers
private BackgroundScreenStack backgroundStack; private BackgroundScreenStack backgroundStack;
private bool allowScaling = true;
/// <summary>
/// Whether user scaling preferences should be applied. Enabled by default.
/// </summary>
public bool AllowScaling
{
get => allowScaling;
set
{
if (value == allowScaling)
return;
allowScaling = value;
if (IsLoaded) updateSize();
}
}
/// <summary> /// <summary>
/// Create a new instance. /// Create a new instance.
/// </summary> /// </summary>
@ -139,7 +157,7 @@ namespace osu.Game.Graphics.Containers
backgroundStack?.FadeOut(fade_time); backgroundStack?.FadeOut(fade_time);
} }
bool scaling = targetMode == null || scalingMode.Value == targetMode; bool scaling = AllowScaling && (targetMode == null || scalingMode.Value == targetMode);
var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One; var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One;
var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero; var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero;

View File

@ -4,11 +4,10 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public abstract class ScoreCounter : RollingCounter<double>, IScoreCounter public abstract class ScoreCounter : RollingCounter<double>
{ {
protected override double RollingDuration => 1000; protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.Out; protected override Easing RollingEasing => Easing.Out;

View File

@ -48,6 +48,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing), new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.ToggleSkinEditor),
new KeyBinding(InputKey.Escape, GlobalAction.Back), new KeyBinding(InputKey.Escape, GlobalAction.Back),
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
@ -258,6 +259,9 @@ namespace osu.Game.Input.Bindings
EditorNudgeLeft, EditorNudgeLeft,
[Description("Nudge selection right")] [Description("Nudge selection right")]
EditorNudgeRight EditorNudgeRight,
[Description("Toggle skin editor")]
ToggleSkinEditor,
} }
} }

View File

@ -6,13 +6,14 @@ using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
namespace osu.Game.Input namespace osu.Game.Input
{ {
/// <summary> /// <summary>
/// Track whether the end-user is in an idle state, based on their last interaction with the game. /// Track whether the end-user is in an idle state, based on their last interaction with the game.
/// </summary> /// </summary>
public class IdleTracker : Component, IKeyBindingHandler<PlatformAction>, IHandleGlobalKeyboardInput public class IdleTracker : Component, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>, IHandleGlobalKeyboardInput
{ {
private readonly double timeToIdle; private readonly double timeToIdle;
@ -58,6 +59,10 @@ namespace osu.Game.Input
public void OnReleased(PlatformAction action) => updateLastInteractionTime(); public void OnReleased(PlatformAction action) => updateLastInteractionTime();
public bool OnPressed(GlobalAction action) => updateLastInteractionTime();
public void OnReleased(GlobalAction action) => updateLastInteractionTime();
protected override bool Handle(UIEvent e) protected override bool Handle(UIEvent e)
{ {
switch (e) switch (e)

View File

@ -23,6 +23,8 @@ namespace osu.Game.Online.API.Requests
return req; return req;
} }
protected override string FileExtension => ".osz";
protected override string Target => $@"beatmapsets/{Model.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}"; protected override string Target => $@"beatmapsets/{Model.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}";
} }
} }

View File

@ -51,6 +51,7 @@ using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel; using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Skinning.Editor;
namespace osu.Game namespace osu.Game
{ {
@ -79,6 +80,8 @@ namespace osu.Game
private BeatmapSetOverlay beatmapSetOverlay; private BeatmapSetOverlay beatmapSetOverlay;
private SkinEditorOverlay skinEditor;
[Cached] [Cached]
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender(); private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
@ -597,6 +600,8 @@ namespace osu.Game
screenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays) screenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
{ {
receptor = new BackButton.Receptor(), receptor = new BackButton.Receptor(),
@ -685,6 +690,7 @@ namespace osu.Game
var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true); var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true); loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
loadComponentSingleFile(skinEditor = new SkinEditorOverlay(screenContainer), overlayContent.Add);
loadComponentSingleFile(new LoginOverlay loadComponentSingleFile(new LoginOverlay
{ {
@ -968,6 +974,8 @@ namespace osu.Game
protected virtual void ScreenChanged(IScreen current, IScreen newScreen) protected virtual void ScreenChanged(IScreen current, IScreen newScreen)
{ {
skinEditor.Reset();
switch (newScreen) switch (newScreen)
{ {
case IntroScreen intro: case IntroScreen intro:

View File

@ -13,6 +13,8 @@ using osuTK.Graphics;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Users;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.BeatmapSet namespace osu.Game.Overlays.BeatmapSet
{ {
@ -50,7 +52,7 @@ namespace osu.Game.Overlays.BeatmapSet
fields.Children = new Drawable[] fields.Children = new Drawable[]
{ {
new Field("mapped by", BeatmapSet.Metadata.Author.Username, OsuFont.GetFont(weight: FontWeight.Regular, italics: true)), new Field("mapped by", BeatmapSet.Metadata.Author, OsuFont.GetFont(weight: FontWeight.Regular, italics: true)),
new Field("submitted", online.Submitted, OsuFont.GetFont(weight: FontWeight.Bold)) new Field("submitted", online.Submitted, OsuFont.GetFont(weight: FontWeight.Bold))
{ {
Margin = new MarginPadding { Top = 5 }, Margin = new MarginPadding { Top = 5 },
@ -146,6 +148,25 @@ namespace osu.Game.Overlays.BeatmapSet
} }
}; };
} }
public Field(string first, User second, FontUsage secondFont)
{
AutoSizeAxes = Axes.Both;
Direction = FillDirection.Horizontal;
Children = new[]
{
new LinkFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 11);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.AddText($"{first} ");
d.AddUserLink(second, s => s.Font = secondFont.With(size: 11));
}),
};
}
} }
} }
} }

View File

@ -100,7 +100,7 @@ namespace osu.Game.Overlays.News.Displays
{ {
content.Add(loaded); content.Add(loaded);
showMore.IsLoading = false; showMore.IsLoading = false;
showMore.Show(); showMore.Alpha = lastCursor == null ? 0 : 1;
}, (cancellationToken = new CancellationTokenSource()).Token); }, (cancellationToken = new CancellationTokenSource()).Token);
} }

View File

@ -141,7 +141,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown(); windowModeDropdown.Current.BindValueChanged(mode =>
{
updateResolutionDropdown();
const string not_fullscreen_note = "Running without fullscreen mode will increase your input latency!";
windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? not_fullscreen_note : string.Empty;
}, true);
windowModes.BindCollectionChanged((sender, args) => windowModes.BindCollectionChanged((sender, args) =>
{ {

View File

@ -13,6 +13,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{ {
protected override string Header => "Renderer"; protected override string Header => "Renderer";
private SettingsEnumDropdown<FrameSync> frameLimiterDropdown;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig) private void load(FrameworkConfigManager config, OsuConfigManager osuConfig)
{ {
@ -20,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
Children = new Drawable[] Children = new Drawable[]
{ {
// TODO: this needs to be a custom dropdown at some point // TODO: this needs to be a custom dropdown at some point
new SettingsEnumDropdown<FrameSync> frameLimiterDropdown = new SettingsEnumDropdown<FrameSync>
{ {
LabelText = "Frame limiter", LabelText = "Frame limiter",
Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync) Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync)
@ -37,5 +39,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}, },
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
frameLimiterDropdown.Current.BindValueChanged(limit =>
{
const string unlimited_frames_note = "Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. \"2x refresh rate\" is recommended.";
frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? unlimited_frames_note : string.Empty;
}, true);
}
} }
} }

View File

@ -2,8 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Users;
namespace osu.Game.Overlays.Settings.Sections.UserInterface namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
@ -11,9 +14,15 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
protected override string Header => "Main Menu"; protected override string Header => "Main Menu";
private IBindable<User> user;
private SettingsEnumDropdown<BackgroundSource> backgroundSourceDropdown;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config, IAPIProvider api)
{ {
user = api.LocalUser.GetBoundCopy();
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsCheckbox new SettingsCheckbox
@ -31,7 +40,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
LabelText = "Intro sequence", LabelText = "Intro sequence",
Current = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence), Current = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence),
}, },
new SettingsEnumDropdown<BackgroundSource> backgroundSourceDropdown = new SettingsEnumDropdown<BackgroundSource>
{ {
LabelText = "Background source", LabelText = "Background source",
Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource), Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource),
@ -43,5 +52,17 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
} }
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
user.BindValueChanged(u =>
{
const string not_supporter_note = "Changes to this setting will only apply with an active osu!supporter tag.";
backgroundSourceDropdown.WarningText = u.NewValue?.IsSupporter != true ? not_supporter_note : string.Empty;
}, true);
}
} }
} }

View File

@ -18,7 +18,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
@ -36,10 +36,15 @@ namespace osu.Game.Overlays.Settings
private SpriteText labelText; private SpriteText labelText;
private OsuTextFlowContainer warningText;
public bool ShowsDefaultIndicator = true; public bool ShowsDefaultIndicator = true;
public string TooltipText { get; set; } public string TooltipText { get; set; }
[Resolved]
private OsuColour colours { get; set; }
public virtual LocalisableString LabelText public virtual LocalisableString LabelText
{ {
get => labelText?.Text ?? string.Empty; get => labelText?.Text ?? string.Empty;
@ -57,6 +62,31 @@ namespace osu.Game.Overlays.Settings
} }
} }
/// <summary>
/// Text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
/// Generally used to recommend the user change their setting as the current one is considered sub-optimal.
/// </summary>
public string WarningText
{
set
{
if (warningText == null)
{
// construct lazily for cases where the label is not needed (may be provided by the Control).
FlowContent.Add(warningText = new OsuTextFlowContainer
{
Colour = colours.Yellow,
Margin = new MarginPadding { Bottom = 5 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
});
}
warningText.Alpha = string.IsNullOrWhiteSpace(value) ? 0 : 1;
warningText.Text = value;
}
}
public virtual Bindable<T> Current public virtual Bindable<T> Current
{ {
get => controlWithCurrent.Current; get => controlWithCurrent.Current;
@ -92,7 +122,10 @@ namespace osu.Game.Overlays.Settings
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
Child = Control = CreateControl() Children = new[]
{
Control = CreateControl(),
},
}, },
}; };
@ -141,6 +174,7 @@ namespace osu.Game.Overlays.Settings
{ {
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Width = SettingsPanel.CONTENT_MARGINS; Width = SettingsPanel.CONTENT_MARGINS;
Padding = new MarginPadding { Vertical = 1.5f };
Alpha = 0f; Alpha = 0f;
} }
@ -163,7 +197,7 @@ namespace osu.Game.Overlays.Settings
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Radius = 2, Radius = 2,
}, },
Size = new Vector2(0.33f, 0.8f), Width = 0.33f,
Child = new Box { RelativeSizeAxes = Axes.Both }, Child = new Box { RelativeSizeAxes = Axes.Both },
}; };
} }
@ -196,12 +230,6 @@ namespace osu.Game.Overlays.Settings
UpdateState(); UpdateState();
} }
public void SetButtonColour(Color4 buttonColour)
{
this.buttonColour = buttonColour;
UpdateState();
}
public void UpdateState() => Scheduler.AddOnce(updateState); public void UpdateState() => Scheduler.AddOnce(updateState);
private void updateState() private void updateState()

View File

@ -69,13 +69,15 @@ namespace osu.Game.Overlays.Toolbar
base.LoadComplete(); base.LoadComplete();
Current.BindDisabledChanged(disabled => this.FadeColour(disabled ? Color4.Gray : Color4.White, 300), true); Current.BindDisabledChanged(disabled => this.FadeColour(disabled ? Color4.Gray : Color4.White, 300), true);
Current.BindValueChanged(_ => moveLineToCurrent(), true); Current.BindValueChanged(_ => moveLineToCurrent());
// Scheduled to allow the button flow layout to be computed before the line position is updated
ScheduleAfterChildren(moveLineToCurrent);
} }
private bool hasInitialPosition; private bool hasInitialPosition;
// Scheduled to allow the flow layout to be computed before the line position is updated private void moveLineToCurrent()
private void moveLineToCurrent() => ScheduleAfterChildren(() =>
{ {
if (SelectedTab != null) if (SelectedTab != null)
{ {
@ -86,7 +88,7 @@ namespace osu.Game.Overlays.Toolbar
hasInitialPosition = true; hasInitialPosition = true;
} }
}); }
public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput; public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput;

View File

@ -204,7 +204,7 @@ namespace osu.Game.Overlays.Volume
{ {
displayVolume = value; displayVolume = value;
if (displayVolume > 0.99f) if (displayVolume >= 0.995f)
{ {
text.Text = "MAX"; text.Text = "MAX";
maxGlow.EffectColour = meterColour.Opacity(2f); maxGlow.EffectColour = meterColour.Opacity(2f);

View File

@ -1,40 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Replays namespace osu.Game.Rulesets.Replays
{ {
public abstract class AutoGenerator : IAutoGenerator public abstract class AutoGenerator
{ {
/// <summary> /// <summary>
/// Creates the auto replay and returns it. /// The default duration of a key press in milliseconds.
/// Every subclass of OsuAutoGeneratorBase should implement this!
/// </summary> /// </summary>
public abstract Replay Generate(); public const double KEY_UP_DELAY = 50;
#region Parameters
/// <summary> /// <summary>
/// The beatmap we're making. /// The beatmap the autoplay is generated for.
/// </summary> /// </summary>
protected IBeatmap Beatmap; protected IBeatmap Beatmap { get; }
#endregion
protected AutoGenerator(IBeatmap beatmap) protected AutoGenerator(IBeatmap beatmap)
{ {
Beatmap = beatmap; Beatmap = beatmap;
} }
#region Constants /// <summary>
/// Generate the replay of the autoplay.
// Shared amongst all modes /// </summary>
public const double KEY_UP_DELAY = 50; public abstract Replay Generate();
#endregion
protected virtual HitObject GetNextObject(int currentIndex) protected virtual HitObject GetNextObject(int currentIndex)
{ {
@ -44,4 +40,37 @@ namespace osu.Game.Rulesets.Replays
return Beatmap.HitObjects[currentIndex + 1]; return Beatmap.HitObjects[currentIndex + 1];
} }
} }
public abstract class AutoGenerator<TFrame> : AutoGenerator
where TFrame : ReplayFrame
{
/// <summary>
/// The replay frames of the autoplay.
/// </summary>
protected readonly List<TFrame> Frames = new List<TFrame>();
[CanBeNull]
protected TFrame LastFrame => Frames.Count == 0 ? null : Frames[^1];
protected AutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
}
public sealed override Replay Generate()
{
Frames.Clear();
GenerateFrames();
return new Replay
{
Frames = Frames.OrderBy(frame => frame.Time).Cast<ReplayFrame>().ToList()
};
}
/// <summary>
/// Generate the replay frames of the autoplay and populate <see cref="Frames"/>.
/// </summary>
protected abstract void GenerateFrames();
}
} }

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
@ -97,7 +98,7 @@ namespace osu.Game.Rulesets.Replays
{ {
// TODO: This replay frame ordering should be enforced on the Replay type. // TODO: This replay frame ordering should be enforced on the Replay type.
// Currently, the ordering can be broken if the frames are added after this construction. // Currently, the ordering can be broken if the frames are added after this construction.
replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time)); replay.Frames = replay.Frames.OrderBy(f => f.Time).ToList();
this.replay = replay; this.replay = replay;
currentFrameIndex = -1; currentFrameIndex = -1;

View File

@ -1,12 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Replays;
namespace osu.Game.Rulesets.Replays
{
public interface IAutoGenerator
{
Replay Generate();
}
}

View File

@ -34,13 +34,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
// For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context.
if (Composer != null)
{
foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject);
}
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects); selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) => selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
{ {
@ -69,7 +62,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (Composer != null) if (Composer != null)
{ {
// For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above.
foreach (var obj in Composer.HitObjects) foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject); AddBlueprintFor(obj.HitObject);

View File

@ -108,17 +108,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected TernaryState GetStateFromSelection<T>(IEnumerable<T> selection, Func<T, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
#endregion #endregion
#region Ternary state changes #region Ternary state changes

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -16,6 +17,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
public class SelectionBox : CompositeDrawable public class SelectionBox : CompositeDrawable
{ {
public const float BORDER_RADIUS = 3;
public Func<float, bool> OnRotation; public Func<float, bool> OnRotation;
public Func<Vector2, Anchor, bool> OnScale; public Func<Vector2, Anchor, bool> OnScale;
public Func<Direction, bool> OnFlip; public Func<Direction, bool> OnFlip;
@ -92,21 +95,32 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
private Container dragHandles; private string text;
public string Text
{
get => text;
set
{
if (value == text)
return;
text = value;
if (selectionDetailsText != null)
selectionDetailsText.Text = value;
}
}
private SelectionBoxDragHandleContainer dragHandles;
private FillFlowContainer buttons; private FillFlowContainer buttons;
public const float BORDER_RADIUS = 3; private OsuSpriteText selectionDetailsText;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load() => recreate();
{
RelativeSizeAxes = Axes.Both;
recreate();
}
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
@ -144,6 +158,26 @@ namespace osu.Game.Screens.Edit.Compose.Components
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Container
{
Name = "info text",
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = colours.YellowDark,
RelativeSizeAxes = Axes.Both,
},
selectionDetailsText = new OsuSpriteText
{
Padding = new MarginPadding(2),
Colour = colours.Gray0,
Font = OsuFont.Default.With(size: 11),
Text = text,
}
}
},
new Container new Container
{ {
Masking = true, Masking = true,
@ -161,7 +195,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}, },
} }
}, },
dragHandles = new Container dragHandles = new SelectionBoxDragHandleContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
// ensures that the centres of all drag handles line up with the middle of the selection box border. // ensures that the centres of all drag handles line up with the middle of the selection box border.
@ -186,75 +220,76 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void addRotationComponents() private void addRotationComponents()
{ {
const float separation = 40;
addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90)); addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90));
addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90)); addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90));
AddRangeInternal(new Drawable[] addRotateHandle(Anchor.TopLeft);
{ addRotateHandle(Anchor.TopRight);
new Box addRotateHandle(Anchor.BottomLeft);
{ addRotateHandle(Anchor.BottomRight);
Depth = float.MaxValue,
Colour = colours.YellowLight,
Blending = BlendingParameters.Additive,
Alpha = 0.3f,
Size = new Vector2(BORDER_RADIUS, separation),
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
},
new SelectionBoxDragHandleButton(FontAwesome.Solid.Redo, "Free rotate")
{
Anchor = Anchor.TopCentre,
Y = -separation,
HandleDrag = e => OnRotation?.Invoke(convertDragEventToAngleOfRotation(e)),
OperationStarted = operationStarted,
OperationEnded = operationEnded
}
});
} }
private void addYScaleComponents() private void addYScaleComponents()
{ {
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical)); addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical));
addDragHandle(Anchor.TopCentre); addScaleHandle(Anchor.TopCentre);
addDragHandle(Anchor.BottomCentre); addScaleHandle(Anchor.BottomCentre);
} }
private void addFullScaleComponents() private void addFullScaleComponents()
{ {
addDragHandle(Anchor.TopLeft); addScaleHandle(Anchor.TopLeft);
addDragHandle(Anchor.TopRight); addScaleHandle(Anchor.TopRight);
addDragHandle(Anchor.BottomLeft); addScaleHandle(Anchor.BottomLeft);
addDragHandle(Anchor.BottomRight); addScaleHandle(Anchor.BottomRight);
} }
private void addXScaleComponents() private void addXScaleComponents()
{ {
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal)); addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal));
addDragHandle(Anchor.CentreLeft); addScaleHandle(Anchor.CentreLeft);
addDragHandle(Anchor.CentreRight); addScaleHandle(Anchor.CentreRight);
} }
private void addButton(IconUsage icon, string tooltip, Action action) private void addButton(IconUsage icon, string tooltip, Action action)
{ {
buttons.Add(new SelectionBoxDragHandleButton(icon, tooltip) var button = new SelectionBoxButton(icon, tooltip)
{ {
OperationStarted = operationStarted,
OperationEnded = operationEnded,
Action = action Action = action
}); };
button.OperationStarted += operationStarted;
button.OperationEnded += operationEnded;
buttons.Add(button);
} }
private void addDragHandle(Anchor anchor) => dragHandles.Add(new SelectionBoxDragHandle private void addScaleHandle(Anchor anchor)
{
var handle = new SelectionBoxScaleHandle
{ {
Anchor = anchor, Anchor = anchor,
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), HandleDrag = e => OnScale?.Invoke(e.Delta, anchor)
OperationStarted = operationStarted, };
OperationEnded = operationEnded
}); handle.OperationStarted += operationStarted;
handle.OperationEnded += operationEnded;
dragHandles.AddScaleHandle(handle);
}
private void addRotateHandle(Anchor anchor)
{
var handle = new SelectionBoxRotationHandle
{
Anchor = anchor,
HandleDrag = e => OnRotation?.Invoke(convertDragEventToAngleOfRotation(e))
};
handle.OperationStarted += operationStarted;
handle.OperationEnded += operationEnded;
dragHandles.AddRotationHandle(handle);
}
private int activeOperations; private int activeOperations;

View File

@ -12,10 +12,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
/// <summary> public sealed class SelectionBoxButton : SelectionBoxControl, IHasTooltip
/// A drag "handle" which shares the visual appearance but behaves more like a clickable button.
/// </summary>
public sealed class SelectionBoxDragHandleButton : SelectionBoxDragHandle, IHasTooltip
{ {
private SpriteIcon icon; private SpriteIcon icon;
@ -23,7 +20,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Action Action; public Action Action;
public SelectionBoxDragHandleButton(IconUsage iconUsage, string tooltip) public SelectionBoxButton(IconUsage iconUsage, string tooltip)
{ {
this.iconUsage = iconUsage; this.iconUsage = iconUsage;
@ -36,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Size *= 2; Size = new Vector2(20);
AddInternal(icon = new SpriteIcon AddInternal(icon = new SpriteIcon
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -49,16 +46,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
OperationStarted?.Invoke(); TriggerOperationStarted();
Action?.Invoke(); Action?.Invoke();
OperationEnded?.Invoke(); TriggerOperatoinEnded();
return true; return true;
} }
protected override void UpdateHoverState() protected override void UpdateHoverState()
{ {
base.UpdateHoverState(); base.UpdateHoverState();
icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black; icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
} }
public string TooltipText { get; } public string TooltipText { get; }

View File

@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// Represents the base appearance for UI controls of the <see cref="SelectionBox"/>,
/// such as scale handles, rotation handles, buttons, etc...
/// </summary>
public abstract class SelectionBoxControl : CompositeDrawable
{
public const double TRANSFORM_DURATION = 100;
public event Action OperationStarted;
public event Action OperationEnded;
private Circle circle;
/// <summary>
/// Whether the user is currently holding the control with mouse.
/// </summary>
public bool IsHeld { get; private set; }
[Resolved]
protected OsuColour Colours { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
UpdateHoverState();
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
UpdateHoverState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
UpdateHoverState();
}
protected override bool OnMouseDown(MouseDownEvent e)
{
IsHeld = true;
UpdateHoverState();
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
IsHeld = false;
UpdateHoverState();
}
protected virtual void UpdateHoverState()
{
if (IsHeld)
circle.FadeColour(Colours.GrayF, TRANSFORM_DURATION, Easing.OutQuint);
else
circle.FadeColour(IsHovered ? Colours.Red : Colours.YellowDark, TRANSFORM_DURATION, Easing.OutQuint);
this.ScaleTo(IsHeld || IsHovered ? 1.5f : 1, TRANSFORM_DURATION, Easing.OutQuint);
}
protected void TriggerOperationStarted() => OperationStarted?.Invoke();
protected void TriggerOperatoinEnded() => OperationEnded?.Invoke();
}
}

View File

@ -2,75 +2,17 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
public class SelectionBoxDragHandle : Container public abstract class SelectionBoxDragHandle : SelectionBoxControl
{ {
public Action OperationStarted;
public Action OperationEnded;
public Action<DragEvent> HandleDrag { get; set; } public Action<DragEvent> HandleDrag { get; set; }
private Circle circle;
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(10);
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
UpdateHoverState();
}
protected override bool OnHover(HoverEvent e)
{
UpdateHoverState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
UpdateHoverState();
}
protected bool HandlingMouse;
protected override bool OnMouseDown(MouseDownEvent e)
{
HandlingMouse = true;
UpdateHoverState();
return true;
}
protected override bool OnDragStart(DragStartEvent e) protected override bool OnDragStart(DragStartEvent e)
{ {
OperationStarted?.Invoke(); TriggerOperationStarted();
return true; return true;
} }
@ -82,24 +24,45 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)
{ {
HandlingMouse = false; TriggerOperatoinEnded();
OperationEnded?.Invoke();
UpdateHoverState(); UpdateHoverState();
base.OnDragEnd(e); base.OnDragEnd(e);
} }
protected override void OnMouseUp(MouseUpEvent e) #region Internal events for SelectionBoxDragHandleContainer
internal event Action HoverGained;
internal event Action HoverLost;
internal event Action MouseDown;
internal event Action MouseUp;
protected override bool OnHover(HoverEvent e)
{ {
HandlingMouse = false; bool result = base.OnHover(e);
UpdateHoverState(); HoverGained?.Invoke();
base.OnMouseUp(e); return result;
} }
protected virtual void UpdateHoverState() protected override void OnHoverLost(HoverLostEvent e)
{ {
circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark); base.OnHoverLost(e);
this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint); HoverLost?.Invoke();
} }
protected override bool OnMouseDown(MouseDownEvent e)
{
bool result = base.OnMouseDown(e);
MouseDown?.Invoke();
return result;
}
protected override void OnMouseUp(MouseUpEvent e)
{
base.OnMouseUp(e);
MouseUp?.Invoke();
}
#endregion
} }
} }

View File

@ -0,0 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// Represents a display composite containing and managing the visibility state of the selection box's drag handles.
/// </summary>
public class SelectionBoxDragHandleContainer : CompositeDrawable
{
private Container<SelectionBoxScaleHandle> scaleHandles;
private Container<SelectionBoxRotationHandle> rotationHandles;
private readonly List<SelectionBoxDragHandle> allDragHandles = new List<SelectionBoxDragHandle>();
public new MarginPadding Padding
{
get => base.Padding;
set => base.Padding = value;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
scaleHandles = new Container<SelectionBoxScaleHandle>
{
RelativeSizeAxes = Axes.Both,
},
rotationHandles = new Container<SelectionBoxRotationHandle>
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(-12.5f),
},
};
}
public void AddScaleHandle(SelectionBoxScaleHandle handle)
{
bindDragHandle(handle);
scaleHandles.Add(handle);
}
public void AddRotationHandle(SelectionBoxRotationHandle handle)
{
handle.Alpha = 0;
handle.AlwaysPresent = true;
bindDragHandle(handle);
rotationHandles.Add(handle);
}
private void bindDragHandle(SelectionBoxDragHandle handle)
{
handle.HoverGained += updateRotationHandlesVisibility;
handle.HoverLost += updateRotationHandlesVisibility;
handle.MouseDown += updateRotationHandlesVisibility;
handle.MouseUp += updateRotationHandlesVisibility;
allDragHandles.Add(handle);
}
private SelectionBoxRotationHandle displayedRotationHandle;
private SelectionBoxDragHandle activeHandle;
private void updateRotationHandlesVisibility()
{
// if the active handle is a rotation handle and is held or hovered,
// then no need to perform any updates to the rotation handles visibility.
if (activeHandle is SelectionBoxRotationHandle && (activeHandle?.IsHeld == true || activeHandle?.IsHovered == true))
return;
displayedRotationHandle?.FadeOut(SelectionBoxControl.TRANSFORM_DURATION, Easing.OutQuint);
displayedRotationHandle = null;
// if the active handle is not a rotation handle but is held, then keep the rotation handle hidden.
if (activeHandle?.IsHeld == true)
return;
activeHandle = rotationHandles.SingleOrDefault(h => h.IsHeld || h.IsHovered);
activeHandle ??= allDragHandles.SingleOrDefault(h => h.IsHovered);
if (activeHandle != null)
{
displayedRotationHandle = getCorrespondingRotationHandle(activeHandle, rotationHandles);
displayedRotationHandle?.FadeIn(SelectionBoxControl.TRANSFORM_DURATION, Easing.OutQuint);
}
}
/// <summary>
/// Gets the rotation handle corresponding to the given handle.
/// </summary>
[CanBeNull]
private static SelectionBoxRotationHandle getCorrespondingRotationHandle(SelectionBoxDragHandle handle, IEnumerable<SelectionBoxRotationHandle> rotationHandles)
{
if (handle is SelectionBoxRotationHandle rotationHandle)
return rotationHandle;
return rotationHandles.SingleOrDefault(r => r.Anchor == handle.Anchor);
}
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class SelectionBoxRotationHandle : SelectionBoxDragHandle
{
private SpriteIcon icon;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(15f);
AddInternal(icon = new SpriteIcon
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Redo,
Scale = new Vector2
{
X = Anchor.HasFlagFast(Anchor.x0) ? 1f : -1f,
Y = Anchor.HasFlagFast(Anchor.y0) ? 1f : -1f
}
});
}
protected override void UpdateHoverState()
{
base.UpdateHoverState();
icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class SelectionBoxScaleHandle : SelectionBoxDragHandle
{
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(10);
}
}
}

View File

@ -10,13 +10,11 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osuTK; using osuTK;
@ -43,10 +41,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly List<SelectionBlueprint<T>> selectedBlueprints; private readonly List<SelectionBlueprint<T>> selectedBlueprints;
private Drawable content;
private OsuSpriteText selectionDetailsText;
protected SelectionBox SelectionBox { get; private set; } protected SelectionBox SelectionBox { get; private set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
@ -58,39 +52,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
AlwaysPresent = true; AlwaysPresent = true;
Alpha = 0;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
InternalChild = content = new Container InternalChild = SelectionBox = CreateSelectionBox();
{
Children = new Drawable[]
{
// todo: should maybe be inside the SelectionBox?
new Container
{
Name = "info text",
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = colours.YellowDark,
RelativeSizeAxes = Axes.Both,
},
selectionDetailsText = new OsuSpriteText
{
Padding = new MarginPadding(2),
Colour = colours.Gray0,
Font = OsuFont.Default.With(size: 11)
}
}
},
SelectionBox = CreateSelectionBox(),
}
};
SelectedItems.CollectionChanged += (sender, args) => SelectedItems.CollectionChanged += (sender, args) =>
{ {
@ -269,6 +236,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
DeleteSelected(); DeleteSelected();
} }
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected static TernaryState GetStateFromSelection<TObject>(IEnumerable<TObject> selection, Func<TObject, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
/// <summary> /// <summary>
/// Called whenever the deletion of items has been requested. /// Called whenever the deletion of items has been requested.
/// </summary> /// </summary>
@ -306,9 +284,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
int count = SelectedItems.Count; int count = SelectedItems.Count;
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; SelectionBox.Text = count > 0 ? count.ToString() : string.Empty;
SelectionBox.FadeTo(count > 0 ? 1 : 0);
this.FadeTo(count > 0 ? 1 : 0);
OnSelectionChanged(); OnSelectionChanged();
} }
@ -335,8 +313,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
selectionRect = selectionRect.Inflate(5f); selectionRect = selectionRect.Inflate(5f);
content.Position = selectionRect.Location; SelectionBox.Position = selectionRect.Location;
content.Size = selectionRect.Size; SelectionBox.Size = selectionRect.Size;
} }
#endregion #endregion

View File

@ -17,7 +17,6 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -144,8 +143,7 @@ namespace osu.Game.Screens.Edit
// Todo: should probably be done at a DrawableRuleset level to share logic with Player. // Todo: should probably be done at a DrawableRuleset level to share logic with Player.
clock = new EditorClock(playableBeatmap, beatDivisor) { IsCoupled = false }; clock = new EditorClock(playableBeatmap, beatDivisor) { IsCoupled = false };
clock.ChangeSource(loadableBeatmap.Track);
UpdateClockSource();
dependencies.CacheAs(clock); dependencies.CacheAs(clock);
AddInternal(clock); AddInternal(clock);
@ -308,11 +306,7 @@ namespace osu.Game.Screens.Edit
/// <summary> /// <summary>
/// If the beatmap's track has changed, this method must be called to keep the editor in a valid state. /// If the beatmap's track has changed, this method must be called to keep the editor in a valid state.
/// </summary> /// </summary>
public void UpdateClockSource() public void UpdateClockSource() => clock.ChangeSource(Beatmap.Value.Track);
{
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock.ChangeSource(sourceClock);
}
protected void Save() protected void Save()
{ {
@ -583,7 +577,7 @@ namespace osu.Game.Screens.Edit
private void resetTrack(bool seekToStart = false) private void resetTrack(bool seekToStart = false)
{ {
Beatmap.Value.Track?.Stop(); Beatmap.Value.Track.Stop();
if (seekToStart) if (seekToStart)
{ {

View File

@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
AllowPause = false, AllowPause = false,
AllowRestart = false, AllowRestart = false,
AllowSkippingIntro = false, AllowSkipping = false,
}) })
{ {
this.userIds = userIds; this.userIds = userIds;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Storyboards; using osu.Game.Storyboards;
@ -19,6 +20,14 @@ namespace osu.Game.Screens.Play
private readonly Storyboard storyboard; private readonly Storyboard storyboard;
private DrawableStoryboard drawableStoryboard; private DrawableStoryboard drawableStoryboard;
/// <summary>
/// Whether the storyboard is considered finished.
/// </summary>
/// <remarks>
/// This is true by default in here, until an actual drawable storyboard is loaded, in which case it'll bind to it.
/// </remarks>
public IBindable<bool> HasStoryboardEnded = new BindableBool(true);
public DimmableStoryboard(Storyboard storyboard) public DimmableStoryboard(Storyboard storyboard)
{ {
this.storyboard = storyboard; this.storyboard = storyboard;
@ -49,6 +58,7 @@ namespace osu.Game.Screens.Play
return; return;
drawableStoryboard = storyboard.CreateDrawable(); drawableStoryboard = storyboard.CreateDrawable();
HasStoryboardEnded.BindTo(drawableStoryboard.HasStoryboardEnded);
if (async) if (async)
LoadComponentAsync(drawableStoryboard, onStoryboardCreated); LoadComponentAsync(drawableStoryboard, onStoryboardCreated);

View File

@ -4,12 +4,11 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK; using osuTK;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultAccuracyCounter : PercentageCounter, IAccuracyCounter public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableComponent
{ {
private readonly Vector2 offset = new Vector2(-20, 5); private readonly Vector2 offset = new Vector2(-20, 5);

View File

@ -7,11 +7,12 @@ using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultComboCounter : RollingCounter<int>, IComboCounter public class DefaultComboCounter : RollingCounter<int>, ISkinnableComponent
{ {
private readonly Vector2 offset = new Vector2(20, 5); private readonly Vector2 offset = new Vector2(20, 5);
@ -24,7 +25,11 @@ namespace osu.Game.Screens.Play.HUD
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.BlueLighter; private void load(OsuColour colours, ScoreProcessor scoreProcessor)
{
Colour = colours.BlueLighter;
Current.BindTo(scoreProcessor.Combo);
}
protected override void Update() protected override void Update()
{ {

View File

@ -16,7 +16,7 @@ using osu.Framework.Utils;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableComponent
{ {
/// <summary> /// <summary>
/// The base opacity of the glow. /// The base opacity of the glow.
@ -107,7 +107,7 @@ namespace osu.Game.Screens.Play.HUD
GlowColour = colours.BlueDarker; GlowColour = colours.BlueDarker;
} }
public override void Flash(JudgementResult result) => Scheduler.AddOnce(flash); protected override void Flash(JudgementResult result) => Scheduler.AddOnce(flash);
private void flash() private void flash()
{ {

View File

@ -4,11 +4,10 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultScoreCounter : ScoreCounter public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableComponent
{ {
public DefaultScoreCounter() public DefaultScoreCounter()
: base(6) : base(6)

View File

@ -39,7 +39,6 @@ namespace osu.Game.Screens.Play.HUD
private readonly Container boxes; private readonly Container boxes;
private Bindable<bool> fadePlayfieldWhenHealthLow; private Bindable<bool> fadePlayfieldWhenHealthLow;
private HealthProcessor healthProcessor;
public FailingLayer() public FailingLayer()
{ {
@ -88,18 +87,10 @@ namespace osu.Game.Screens.Play.HUD
updateState(); updateState();
} }
public override void BindHealthProcessor(HealthProcessor processor)
{
base.BindHealthProcessor(processor);
healthProcessor = processor;
updateState();
}
private void updateState() private void updateState()
{ {
// Don't display ever if the ruleset is not using a draining health display. // Don't display ever if the ruleset is not using a draining health display.
var showLayer = healthProcessor is DrainingHealthProcessor && fadePlayfieldWhenHealthLow.Value && ShowHealth.Value; var showLayer = HealthProcessor is DrainingHealthProcessor && fadePlayfieldWhenHealthLow.Value && ShowHealth.Value;
this.FadeTo(showLayer ? 1 : 0, fade_time, Easing.OutQuint); this.FadeTo(showLayer ? 1 : 0, fade_time, Easing.OutQuint);
} }

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD
{
public abstract class GameplayAccuracyCounter : PercentageCounter
{
[BackgroundDependencyLoader]
private void load(ScoreProcessor scoreProcessor)
{
Current.BindTo(scoreProcessor.Accuracy);
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD
{
public abstract class GameplayScoreCounter : ScoreCounter
{
private Bindable<ScoringMode> scoreDisplayMode;
protected GameplayScoreCounter(int leading = 0, bool useCommaSeparator = false)
: base(leading, useCommaSeparator)
{
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, ScoreProcessor scoreProcessor)
{
scoreDisplayMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
scoreDisplayMode.BindValueChanged(scoreMode =>
{
switch (scoreMode.NewValue)
{
case ScoringMode.Standardised:
RequiredDisplayDigits.Value = 6;
break;
case ScoringMode.Classic:
RequiredDisplayDigits.Value = 8;
break;
default:
throw new ArgumentOutOfRangeException(nameof(scoreMode));
}
}, true);
Current.BindTo(scoreProcessor.TotalScore);
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -11,26 +12,43 @@ namespace osu.Game.Screens.Play.HUD
{ {
/// <summary> /// <summary>
/// A container for components displaying the current player health. /// A container for components displaying the current player health.
/// Gets bound automatically to the <see cref="HealthProcessor"/> when inserted to <see cref="DrawableRuleset.Overlays"/> hierarchy. /// Gets bound automatically to the <see cref="Rulesets.Scoring.HealthProcessor"/> when inserted to <see cref="DrawableRuleset.Overlays"/> hierarchy.
/// </summary> /// </summary>
public abstract class HealthDisplay : Container, IHealthDisplay public abstract class HealthDisplay : Container
{ {
[Resolved]
protected HealthProcessor HealthProcessor { get; private set; }
public Bindable<double> Current { get; } = new BindableDouble(1) public Bindable<double> Current { get; } = new BindableDouble(1)
{ {
MinValue = 0, MinValue = 0,
MaxValue = 1 MaxValue = 1
}; };
public virtual void Flash(JudgementResult result) protected virtual void Flash(JudgementResult result)
{ {
} }
/// <summary> [BackgroundDependencyLoader]
/// Bind the tracked fields of <see cref="HealthProcessor"/> to this health display. private void load()
/// </summary>
public virtual void BindHealthProcessor(HealthProcessor processor)
{ {
Current.BindTo(processor.Health); Current.BindTo(HealthProcessor.Health);
HealthProcessor.NewJudgement += onNewJudgement;
}
private void onNewJudgement(JudgementResult judgement)
{
if (judgement.IsHit && judgement.Type != HitResult.IgnoreHit)
Flash(judgement);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (HealthProcessor != null)
HealthProcessor.NewJudgement -= onNewJudgement;
} }
} }
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
@ -22,17 +21,11 @@ namespace osu.Game.Screens.Play.HUD
private readonly HitWindows hitWindows; private readonly HitWindows hitWindows;
private readonly ScoreProcessor processor; public HitErrorDisplay(HitWindows hitWindows)
public HitErrorDisplay(ScoreProcessor processor, HitWindows hitWindows)
{ {
this.processor = processor;
this.hitWindows = hitWindows; this.hitWindows = hitWindows;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
if (processor != null)
processor.NewJudgement += onNewJudgement;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -47,15 +40,6 @@ namespace osu.Game.Screens.Play.HUD
type.BindValueChanged(typeChanged, true); type.BindValueChanged(typeChanged, true);
} }
private void onNewJudgement(JudgementResult result)
{
if (result.HitObject.HitWindows.WindowFor(HitResult.Miss) == 0)
return;
foreach (var c in Children)
c.OnNewJudgement(result);
}
private void typeChanged(ValueChangedEvent<ScoreMeterType> type) private void typeChanged(ValueChangedEvent<ScoreMeterType> type)
{ {
Children.ForEach(c => c.FadeOut(fade_duration, Easing.OutQuint)); Children.ForEach(c => c.FadeOut(fade_duration, Easing.OutQuint));
@ -139,13 +123,5 @@ namespace osu.Game.Screens.Play.HUD
Add(display); Add(display);
display.FadeInFromZero(fade_duration, Easing.OutQuint); display.FadeInFromZero(fade_duration, Easing.OutQuint);
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (processor != null)
processor.NewJudgement -= onNewJudgement;
}
} }
} }

View File

@ -18,7 +18,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD.HitErrorMeters namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{ {
public class BarHitErrorMeter : HitErrorMeter public class BarHitErrorMeter : HitErrorMeter, ISkinnableComponent
{ {
private readonly Anchor alignment; private readonly Anchor alignment;
@ -214,7 +214,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
private const int max_concurrent_judgements = 50; private const int max_concurrent_judgements = 50;
public override void OnNewJudgement(JudgementResult judgement) protected override void OnNewJudgement(JudgementResult judgement)
{ {
if (!judgement.IsHit) if (!judgement.IsHit)
return; return;

View File

@ -26,7 +26,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
InternalChild = judgementsFlow = new JudgementFlow(); InternalChild = judgementsFlow = new JudgementFlow();
} }
public override void OnNewJudgement(JudgementResult judgement) => judgementsFlow.Push(GetColourForHitResult(HitWindows.ResultFor(judgement.TimeOffset))); protected override void OnNewJudgement(JudgementResult judgement) => judgementsFlow.Push(GetColourForHitResult(HitWindows.ResultFor(judgement.TimeOffset)));
private class JudgementFlow : FillFlowContainer<HitErrorCircle> private class JudgementFlow : FillFlowContainer<HitErrorCircle>
{ {

View File

@ -14,6 +14,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{ {
protected readonly HitWindows HitWindows; protected readonly HitWindows HitWindows;
[Resolved]
private ScoreProcessor processor { get; set; }
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -22,7 +25,22 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
HitWindows = hitWindows; HitWindows = hitWindows;
} }
public abstract void OnNewJudgement(JudgementResult judgement); protected override void LoadComplete()
{
base.LoadComplete();
processor.NewJudgement += onNewJudgement;
}
private void onNewJudgement(JudgementResult result)
{
if (result.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0)
return;
OnNewJudgement(result);
}
protected abstract void OnNewJudgement(JudgementResult judgement);
protected Color4 GetColourForHitResult(HitResult result) protected Color4 GetColourForHitResult(HitResult result)
{ {
@ -47,5 +65,13 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
return colours.BlueLight; return colours.BlueLight;
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (processor != null)
processor.NewJudgement -= onNewJudgement;
}
} }
} }

View File

@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a accuracy counter.
/// </summary>
public interface IAccuracyCounter : IDrawable
{
/// <summary>
/// The current accuracy to be displayed.
/// </summary>
Bindable<double> Current { get; }
}
}

View File

@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a combo counter.
/// </summary>
public interface IComboCounter : IDrawable
{
/// <summary>
/// The current combo to be displayed.
/// </summary>
Bindable<int> Current { get; }
}
}

View File

@ -1,26 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a health display.
/// </summary>
public interface IHealthDisplay : IDrawable
{
/// <summary>
/// The current health to be displayed.
/// </summary>
Bindable<double> Current { get; }
/// <summary>
/// Flash the display for a specified result type.
/// </summary>
/// <param name="result">The result type.</param>
void Flash(JudgementResult result);
}
}

View File

@ -1,25 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// An interface providing a set of methods to update a score counter.
/// </summary>
public interface IScoreCounter : IDrawable
{
/// <summary>
/// The current score to be displayed.
/// </summary>
Bindable<double> Current { get; }
/// <summary>
/// The number of digits required to display most sane scores.
/// This may be exceeded in very rare cases, but is useful to pad or space the display to avoid it jumping around.
/// </summary>
Bindable<int> RequiredDisplayDigits { get; }
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications.
/// </summary>
public interface ISkinnableComponent : IDrawable
{
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -14,7 +15,7 @@ namespace osu.Game.Screens.Play.HUD
/// <summary> /// <summary>
/// Uses the 'x' symbol and has a pop-out effect while rolling over. /// Uses the 'x' symbol and has a pop-out effect while rolling over.
/// </summary> /// </summary>
public class LegacyComboCounter : CompositeDrawable, IComboCounter public class LegacyComboCounter : CompositeDrawable, ISkinnableComponent
{ {
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0, }; public Bindable<int> Current { get; } = new BindableInt { MinValue = 0, };
@ -79,7 +80,7 @@ namespace osu.Game.Screens.Play.HUD
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(ScoreProcessor scoreProcessor)
{ {
InternalChildren = new[] InternalChildren = new[]
{ {
@ -95,7 +96,7 @@ namespace osu.Game.Screens.Play.HUD
}, },
}; };
Current.ValueChanged += combo => updateCount(combo.NewValue == 0); Current.BindTo(scoreProcessor.Combo);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -109,7 +110,7 @@ namespace osu.Game.Screens.Play.HUD
popOutCount.Origin = Origin; popOutCount.Origin = Origin;
popOutCount.Anchor = Anchor; popOutCount.Anchor = Anchor;
updateCount(false); Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true);
} }
private void updateCount(bool rolling) private void updateCount(bool rolling)

View File

@ -1,29 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class SkinnableAccuracyCounter : SkinnableDrawable, IAccuracyCounter public class SkinnableAccuracyCounter : SkinnableDrawable
{ {
public Bindable<double> Current { get; } = new Bindable<double>();
public SkinnableAccuracyCounter() public SkinnableAccuracyCounter()
: base(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter), _ => new DefaultAccuracyCounter()) : base(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter), _ => new DefaultAccuracyCounter())
{ {
CentreComponent = false; CentreComponent = false;
} }
private IAccuracyCounter skinnedCounter;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
skinnedCounter = Drawable as IAccuracyCounter;
skinnedCounter?.Current.BindTo(Current);
}
} }
} }

View File

@ -1,29 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class SkinnableComboCounter : SkinnableDrawable, IComboCounter public class SkinnableComboCounter : SkinnableDrawable
{ {
public Bindable<int> Current { get; } = new Bindable<int>();
public SkinnableComboCounter() public SkinnableComboCounter()
: base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), skinComponent => new DefaultComboCounter()) : base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), skinComponent => new DefaultComboCounter())
{ {
CentreComponent = false; CentreComponent = false;
} }
private IComboCounter skinnedCounter;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
skinnedCounter = Drawable as IComboCounter;
skinnedCounter?.Current.BindTo(Current);
}
} }
} }

View File

@ -1,50 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class SkinnableHealthDisplay : SkinnableDrawable, IHealthDisplay public class SkinnableHealthDisplay : SkinnableDrawable
{ {
public Bindable<double> Current { get; } = new BindableDouble(1)
{
MinValue = 0,
MaxValue = 1
};
public void Flash(JudgementResult result) => skinnedCounter?.Flash(result);
private HealthProcessor processor;
public void BindHealthProcessor(HealthProcessor processor)
{
if (this.processor != null)
throw new InvalidOperationException("Can't bind to a processor more than once");
this.processor = processor;
Current.BindTo(processor.Health);
}
public SkinnableHealthDisplay() public SkinnableHealthDisplay()
: base(new HUDSkinComponent(HUDSkinComponents.HealthDisplay), _ => new DefaultHealthDisplay()) : base(new HUDSkinComponent(HUDSkinComponents.HealthDisplay), _ => new DefaultHealthDisplay())
{ {
CentreComponent = false; CentreComponent = false;
} }
private IHealthDisplay skinnedCounter;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
skinnedCounter = Drawable as IHealthDisplay;
skinnedCounter?.Current.BindTo(Current);
}
} }
} }

View File

@ -1,61 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class SkinnableScoreCounter : SkinnableDrawable, IScoreCounter public class SkinnableScoreCounter : SkinnableDrawable
{ {
public Bindable<double> Current { get; } = new Bindable<double>();
private Bindable<ScoringMode> scoreDisplayMode;
public Bindable<int> RequiredDisplayDigits { get; } = new Bindable<int>();
public SkinnableScoreCounter() public SkinnableScoreCounter()
: base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter()) : base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter())
{ {
CentreComponent = false; CentreComponent = false;
} }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
scoreDisplayMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
scoreDisplayMode.BindValueChanged(scoreMode =>
{
switch (scoreMode.NewValue)
{
case ScoringMode.Standardised:
RequiredDisplayDigits.Value = 6;
break;
case ScoringMode.Classic:
RequiredDisplayDigits.Value = 8;
break;
default:
throw new ArgumentOutOfRangeException(nameof(scoreMode));
}
}, true);
}
private IScoreCounter skinnedCounter;
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
{
base.SkinChanged(skin, allowFallback);
skinnedCounter = Drawable as IScoreCounter;
skinnedCounter?.Current.BindTo(Current);
skinnedCounter?.RequiredDisplayDigits.BindTo(RequiredDisplayDigits);
}
} }
} }

View File

@ -14,7 +14,6 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osuTK; using osuTK;
@ -34,21 +33,16 @@ namespace osu.Game.Screens.Play
public float TopScoringElementsHeight { get; private set; } public float TopScoringElementsHeight { get; private set; }
public readonly KeyCounterDisplay KeyCounter; public readonly KeyCounterDisplay KeyCounter;
public readonly SkinnableComboCounter ComboCounter;
public readonly SkinnableScoreCounter ScoreCounter; public readonly SkinnableScoreCounter ScoreCounter;
public readonly SkinnableAccuracyCounter AccuracyCounter; public readonly SkinnableAccuracyCounter AccuracyCounter;
public readonly SkinnableHealthDisplay HealthDisplay; public readonly SkinnableHealthDisplay HealthDisplay;
public readonly SongProgress Progress; public readonly SongProgress Progress;
public readonly ModDisplay ModDisplay; public readonly ModDisplay ModDisplay;
public readonly HitErrorDisplay HitErrorDisplay;
public readonly HoldForMenuButton HoldToQuit; public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay; public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
public readonly FailingLayer FailingLayer;
public Bindable<bool> ShowHealthbar = new Bindable<bool>(true); public Bindable<bool> ShowHealthbar = new Bindable<bool>(true);
private readonly ScoreProcessor scoreProcessor;
private readonly HealthProcessor healthProcessor;
private readonly DrawableRuleset drawableRuleset; private readonly DrawableRuleset drawableRuleset;
private readonly IReadOnlyList<Mod> mods; private readonly IReadOnlyList<Mod> mods;
@ -76,10 +70,8 @@ namespace osu.Game.Screens.Play
private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements }; private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements };
public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods) public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
{ {
this.scoreProcessor = scoreProcessor;
this.healthProcessor = healthProcessor;
this.drawableRuleset = drawableRuleset; this.drawableRuleset = drawableRuleset;
this.mods = mods; this.mods = mods;
@ -87,7 +79,7 @@ namespace osu.Game.Screens.Play
Children = new Drawable[] Children = new Drawable[]
{ {
FailingLayer = CreateFailingLayer(), CreateFailingLayer(),
visibilityContainer = new Container visibilityContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -106,8 +98,8 @@ namespace osu.Game.Screens.Play
HealthDisplay = CreateHealthDisplay(), HealthDisplay = CreateHealthDisplay(),
AccuracyCounter = CreateAccuracyCounter(), AccuracyCounter = CreateAccuracyCounter(),
ScoreCounter = CreateScoreCounter(), ScoreCounter = CreateScoreCounter(),
ComboCounter = CreateComboCounter(), CreateComboCounter(),
HitErrorDisplay = CreateHitErrorDisplayOverlay(), CreateHitErrorDisplayOverlay(),
} }
}, },
}, },
@ -159,12 +151,6 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuConfigManager config, NotificationOverlay notificationOverlay) private void load(OsuConfigManager config, NotificationOverlay notificationOverlay)
{ {
if (scoreProcessor != null)
BindScoreProcessor(scoreProcessor);
if (healthProcessor != null)
BindHealthProcessor(healthProcessor);
if (drawableRuleset != null) if (drawableRuleset != null)
{ {
BindDrawableRuleset(drawableRuleset); BindDrawableRuleset(drawableRuleset);
@ -276,13 +262,13 @@ namespace osu.Game.Screens.Play
Progress.BindDrawableRuleset(drawableRuleset); Progress.BindDrawableRuleset(drawableRuleset);
} }
protected virtual SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter(); protected SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter();
protected virtual SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter(); protected SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter();
protected virtual SkinnableComboCounter CreateComboCounter() => new SkinnableComboCounter(); protected SkinnableComboCounter CreateComboCounter() => new SkinnableComboCounter();
protected virtual SkinnableHealthDisplay CreateHealthDisplay() => new SkinnableHealthDisplay(); protected SkinnableHealthDisplay CreateHealthDisplay() => new SkinnableHealthDisplay();
protected virtual FailingLayer CreateFailingLayer() => new FailingLayer protected virtual FailingLayer CreateFailingLayer() => new FailingLayer
{ {
@ -315,32 +301,10 @@ namespace osu.Game.Screens.Play
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
}; };
protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset?.FirstAvailableHitWindows); protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(drawableRuleset?.FirstAvailableHitWindows);
protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay(); protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
protected virtual void BindScoreProcessor(ScoreProcessor processor)
{
ScoreCounter?.Current.BindTo(processor.TotalScore);
AccuracyCounter?.Current.BindTo(processor.Accuracy);
ComboCounter?.Current.BindTo(processor.Combo);
if (HealthDisplay is IHealthDisplay shd)
{
processor.NewJudgement += judgement =>
{
if (judgement.IsHit && judgement.Type != HitResult.IgnoreHit)
shd.Flash(judgement);
};
}
}
protected virtual void BindHealthProcessor(HealthProcessor processor)
{
HealthDisplay?.BindHealthProcessor(processor);
FailingLayer?.BindHealthProcessor(processor);
}
public bool OnPressed(GlobalAction action) public bool OnPressed(GlobalAction action)
{ {
switch (action) switch (action)

View File

@ -104,7 +104,8 @@ namespace osu.Game.Screens.Play
private BreakTracker breakTracker; private BreakTracker breakTracker;
private SkipOverlay skipOverlay; private SkipOverlay skipIntroOverlay;
private SkipOverlay skipOutroOverlay;
protected ScoreProcessor ScoreProcessor { get; private set; } protected ScoreProcessor ScoreProcessor { get; private set; }
@ -202,9 +203,13 @@ namespace osu.Game.Screens.Play
ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.ApplyBeatmap(playableBeatmap);
ScoreProcessor.Mods.BindTo(Mods); ScoreProcessor.Mods.BindTo(Mods);
dependencies.CacheAs(ScoreProcessor);
HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime);
HealthProcessor.ApplyBeatmap(playableBeatmap); HealthProcessor.ApplyBeatmap(playableBeatmap);
dependencies.CacheAs(HealthProcessor);
if (!ScoreProcessor.Mode.Disabled) if (!ScoreProcessor.Mode.Disabled)
config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
@ -244,7 +249,6 @@ namespace osu.Game.Screens.Play
HUDOverlay.ShowHud.Value = false; HUDOverlay.ShowHud.Value = false;
HUDOverlay.ShowHud.Disabled = true; HUDOverlay.ShowHud.Disabled = true;
BreakOverlay.Hide(); BreakOverlay.Hide();
skipOverlay.Hide();
} }
DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting => DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting =>
@ -281,8 +285,14 @@ namespace osu.Game.Screens.Play
ScoreProcessor.RevertResult(r); ScoreProcessor.RevertResult(r);
}; };
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{
if (storyboardEnded.NewValue && completionProgressDelegate == null)
updateCompletionState();
};
// Bind the judgement processors to ourselves // Bind the judgement processors to ourselves
ScoreProcessor.HasCompleted.ValueChanged += updateCompletionState; ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
HealthProcessor.Failed += onFail; HealthProcessor.Failed += onFail;
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>()) foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
@ -335,7 +345,7 @@ namespace osu.Game.Screens.Play
// display the cursor above some HUD elements. // display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
HUDOverlay = new HUDOverlay(ScoreProcessor, HealthProcessor, DrawableRuleset, Mods.Value) HUDOverlay = new HUDOverlay(DrawableRuleset, Mods.Value)
{ {
HoldToQuit = HoldToQuit =
{ {
@ -355,10 +365,15 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre
}, },
skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
{ {
RequestSkip = performUserRequestedSkip RequestSkip = performUserRequestedSkip
}, },
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
{
RequestSkip = () => updateCompletionState(true),
Alpha = 0
},
FailOverlay = new FailOverlay FailOverlay = new FailOverlay
{ {
OnRetry = Restart, OnRetry = Restart,
@ -385,12 +400,15 @@ namespace osu.Game.Screens.Play
} }
}; };
if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays)
{
skipIntroOverlay.Expire();
skipOutroOverlay.Expire();
}
if (GameplayClockContainer is MasterGameplayClockContainer master) if (GameplayClockContainer is MasterGameplayClockContainer master)
HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate; HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate;
if (!Configuration.AllowSkippingIntro)
skipOverlay.Expire();
if (Configuration.AllowRestart) if (Configuration.AllowRestart)
{ {
container.Add(new HotkeyRetryOverlay container.Add(new HotkeyRetryOverlay
@ -525,6 +543,12 @@ namespace osu.Game.Screens.Play
Pause(); Pause();
return; return;
} }
// if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting.
if (prepareScoreForDisplayTask != null && completionProgressDelegate == null)
{
updateCompletionState(true);
}
} }
this.Exit(); this.Exit();
@ -564,17 +588,23 @@ namespace osu.Game.Screens.Play
private ScheduledDelegate completionProgressDelegate; private ScheduledDelegate completionProgressDelegate;
private Task<ScoreInfo> prepareScoreForDisplayTask; private Task<ScoreInfo> prepareScoreForDisplayTask;
private void updateCompletionState(ValueChangedEvent<bool> completionState) /// <summary>
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
/// </summary>
/// <param name="skipStoryboardOutro">If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.</param>
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
private void updateCompletionState(bool skipStoryboardOutro = false)
{ {
// screen may be in the exiting transition phase. // screen may be in the exiting transition phase.
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
return; return;
if (!completionState.NewValue) if (!ScoreProcessor.HasCompleted.Value)
{ {
completionProgressDelegate?.Cancel(); completionProgressDelegate?.Cancel();
completionProgressDelegate = null; completionProgressDelegate = null;
ValidForResume = true; ValidForResume = true;
skipOutroOverlay.Hide();
return; return;
} }
@ -614,6 +644,20 @@ namespace osu.Game.Screens.Play
return score.ScoreInfo; return score.ScoreInfo;
}); });
if (skipStoryboardOutro)
{
scheduleCompletion();
return;
}
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
if (storyboardHasOutro)
{
skipOutroOverlay.Show();
return;
}
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
scheduleCompletion(); scheduleCompletion();
} }

View File

@ -21,8 +21,8 @@ namespace osu.Game.Screens.Play
public bool AllowRestart { get; set; } = true; public bool AllowRestart { get; set; } = true;
/// <summary> /// <summary>
/// Whether the player should be allowed to skip the intro, advancing to the start of gameplay. /// Whether the player should be allowed to skip intros/outros, advancing to the start of gameplay or the end of a storyboard.
/// </summary> /// </summary>
public bool AllowSkippingIntro { get; set; } = true; public bool AllowSkipping { get; set; } = true;
} }
} }

View File

@ -8,19 +8,19 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Input.Bindings;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
@ -92,6 +92,18 @@ namespace osu.Game.Screens.Play
private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME; private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME;
public override void Hide()
{
base.Hide();
fadeContainer.Hide();
}
public override void Show()
{
base.Show();
fadeContainer.Show();
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -147,7 +159,7 @@ namespace osu.Game.Screens.Play
{ {
} }
private class FadeContainer : Container, IStateful<Visibility> public class FadeContainer : Container, IStateful<Visibility>
{ {
public event Action<Visibility> StateChanged; public event Action<Visibility> StateChanged;
@ -170,7 +182,7 @@ namespace osu.Game.Screens.Play
switch (state) switch (state)
{ {
case Visibility.Visible: case Visibility.Visible:
// we may be triggered to become visible mnultiple times but we only want to transform once. // we may be triggered to become visible multiple times but we only want to transform once.
if (stateChanged) if (stateChanged)
this.FadeIn(500, Easing.OutExpo); this.FadeIn(500, Easing.OutExpo);

View File

@ -14,6 +14,7 @@ using osu.Framework.Timing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
@ -71,8 +72,14 @@ namespace osu.Game.Screens.Play
public SongProgress() public SongProgress()
{ {
Masking = true; Children = new Drawable[]
{
new SongProgressDisplay
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Children = new Drawable[] Children = new Drawable[]
{ {
info = new SongProgressInfo info = new SongProgressInfo
@ -96,6 +103,8 @@ namespace osu.Game.Screens.Play
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
OnSeek = time => RequestSeek?.Invoke(time), OnSeek = time => RequestSeek?.Invoke(time),
}, },
}
},
}; };
} }
@ -175,5 +184,11 @@ namespace osu.Game.Screens.Play
float finalMargin = bottom_bar_height + (AllowSeeking.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0); float finalMargin = bottom_bar_height + (AllowSeeking.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0);
info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In); info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In);
} }
public class SongProgressDisplay : Container, ISkinnableComponent
{
// TODO: move actual implementation into this.
// exists for skin customisation purposes.
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More