Merge branch 'master' into skin-editor-button-access

This commit is contained in:
Bartłomiej Dach 2021-07-22 22:49:44 +02:00
commit b1087d14f3
No known key found for this signature in database
GPG Key ID: BCECCD4FA41F6497
108 changed files with 2305 additions and 789 deletions

View File

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Dual client test" type="CompoundRunConfigurationType">
<toRun name="osu!" type="DotNetProject" />
<toRun name="osu! (Second Client)" type="DotNetProject" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="osu! (Second Client)" type="DotNetProject" factoryName=".NET Project" folderName="osu!" activateToolWindowBeforeRun="false">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0/osu!.dll" />
<option name="PROGRAM_PARAMETERS" value="--debug-client-id=1" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/net5.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/osu.Desktop/osu.Desktop.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net5.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.706.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.722.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.714.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.721.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -3,7 +3,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework; using osu.Framework;
@ -17,13 +16,43 @@ namespace osu.Desktop
{ {
public static class Program public static class Program
{ {
private const string base_game_name = @"osu";
[STAThread] [STAThread]
public static int Main(string[] args) public static int Main(string[] args)
{ {
// Back up the cwd before DesktopGameHost changes it // Back up the cwd before DesktopGameHost changes it
var cwd = Environment.CurrentDirectory; var cwd = Environment.CurrentDirectory;
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) string gameName = base_game_name;
bool tournamentClient = false;
foreach (var arg in args)
{
var split = arg.Split('=');
var key = split[0];
var val = split[1];
switch (key)
{
case "--tournament":
tournamentClient = true;
break;
case "--debug-client-id":
if (!DebugUtils.IsDebugBuild)
throw new InvalidOperationException("Cannot use this argument in a non-debug build.");
if (!int.TryParse(val, out int clientID))
throw new ArgumentException("Provided client ID must be an integer.");
gameName = $"{base_game_name}-{clientID}";
break;
}
}
using (DesktopGameHost host = Host.GetSuitableHost(gameName, true))
{ {
host.ExceptionThrown += handleException; host.ExceptionThrown += handleException;
@ -48,16 +77,10 @@ namespace osu.Desktop
return 0; return 0;
} }
switch (args.FirstOrDefault() ?? string.Empty) if (tournamentClient)
{ host.Run(new TournamentGame());
default: else
host.Run(new OsuGameDesktop(args)); host.Run(new OsuGameDesktop(args));
break;
case "--tournament":
host.Run(new TournamentGame());
break;
}
return 0; return 0;
} }

View File

@ -1,10 +1,17 @@
// 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.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Editor namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
@ -14,11 +21,52 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
protected override Container<Drawable> Content => contentContainer; protected override Container<Drawable> Content => contentContainer;
[Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
protected readonly EditorBeatmap EditorBeatmap;
private readonly CatchEditorTestSceneContainer contentContainer; private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchSelectionBlueprintTestScene() protected CatchSelectionBlueprintTestScene()
{ {
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer()); EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
{
BeatLength = 100
});
base.Content.Add(new EditorBeatmapDependencyContainer(EditorBeatmap, new BindableBeatDivisor())
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
EditorBeatmap,
contentContainer = new CatchEditorTestSceneContainer()
},
});
}
protected void AddMouseMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
{
float y = HitObjectContainer.PositionAtTime(time);
Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
InputManager.MoveMouseTo(pos);
});
private class EditorBeatmapDependencyContainer : Container
{
[Cached]
private readonly EditorClock editorClock;
[Cached]
private readonly BindableBeatDivisor beatDivisor;
public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
{
editorClock = new EditorClock(beatmap, beatDivisor);
this.beatDivisor = beatDivisor;
}
} }
} }
} }

View File

@ -1,38 +1,286 @@
// 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.Game.Beatmaps; using System.Collections.Generic;
using osu.Game.Beatmaps.ControlPoints; using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
{ {
public TestSceneJuiceStreamSelectionBlueprint() private JuiceStream hitObject;
private readonly ManualClock manualClock = new ManualClock();
[SetUp]
public void SetUp() => Schedule(() =>
{ {
var hitObject = new JuiceStream EditorBeatmap.Clear();
Content.Clear();
manualClock.CurrentTime = 0;
Content.Clock = new FramedClock(manualClock);
InputManager.ReleaseButton(MouseButton.Left);
InputManager.ReleaseKey(Key.ShiftLeft);
InputManager.ReleaseKey(Key.ControlLeft);
});
[Test]
public void TestBasicComponentLayout()
{
double[] times = { 100, 300, 500 };
float[] positions = { 100, 200, 100 };
addBlueprintStep(times, positions);
for (int i = 0; i < times.Length; i++)
addVertexCheckStep(times.Length, i, times[i], positions[i]);
AddAssert("correct outline count", () =>
{ {
OriginalX = 100, var expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
StartTime = 100, return this.ChildrenOfType<FruitOutline>().Count() == expected;
Path = new SliderPath(PathType.PerfectCurve, new[] });
AddAssert("correct vertex piece count", () =>
this.ChildrenOfType<VertexPiece>().Count() == times.Length);
AddAssert("first vertex is semitransparent", () =>
Precision.DefinitelyBigger(1, this.ChildrenOfType<VertexPiece>().First().Alpha));
}
[Test]
public void TestVertexDrag()
{
double[] times = { 100, 400, 700 };
float[] positions = { 100, 100, 100 };
addBlueprintStep(times, positions);
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(500, 150);
addVertexCheckStep(3, 1, 500, 150);
addDragEndStep();
addDragStartStep(times[2], positions[2]);
AddMouseMoveStep(300, 50);
addVertexCheckStep(3, 1, 300, 50);
addVertexCheckStep(3, 2, 500, 150);
AddMouseMoveStep(-100, 100);
addVertexCheckStep(3, 1, times[0], positions[0]);
}
[Test]
public void TestMultipleDrag()
{
double[] times = { 100, 300, 500, 700 };
float[] positions = { 100, 100, 100, 100 };
addBlueprintStep(times, positions);
AddMouseMoveStep(times[1], positions[1]);
AddStep("press left", () => InputManager.PressButton(MouseButton.Left));
AddStep("release left", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
addDragStartStep(times[2], positions[2]);
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
}
[Test]
public void TestClampedPositionIsRestored()
{
const double velocity = 0.25;
double[] times = { 100, 500, 700 };
float[] positions = { 100, 100, 100 };
addBlueprintStep(times, positions, velocity);
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 200);
addVertexCheckStep(3, 1, times[1], 200);
addVertexCheckStep(3, 2, times[2], 150);
AddMouseMoveStep(times[1], 100);
addVertexCheckStep(3, 1, times[1], 100);
// Stored position is restored.
addVertexCheckStep(3, 2, times[2], positions[2]);
AddMouseMoveStep(times[1], 300);
addDragEndStep();
addDragStartStep(times[1], 300);
AddMouseMoveStep(times[1], 100);
// Position is different because a changed position is committed when the previous drag is ended.
addVertexCheckStep(3, 2, times[2], 250);
}
[Test]
public void TestScrollWhileDrag()
{
double[] times = { 300, 500 };
float[] positions = { 100, 100 };
addBlueprintStep(times, positions);
addDragStartStep(times[1], positions[1]);
// This mouse move is necessary to start drag and capture the input.
AddMouseMoveStep(times[1], positions[1] + 50);
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
}
[Test]
public void TestUpdateFromHitObject()
{
double[] times = { 100, 300 };
float[] positions = { 200, 200 };
addBlueprintStep(times, positions);
AddStep("update hit object path", () =>
{
hitObject.Path = new SliderPath(PathType.PerfectCurve, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(200, 100), new Vector2(100, 100),
new Vector2(0, 200), new Vector2(0, 200),
}), });
}; EditorBeatmap.Update(hitObject);
var controlPoint = new ControlPointInfo();
controlPoint.Add(0, new TimingControlPoint
{
BeatLength = 100
}); });
hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 }); AddAssert("path is updated", () => getVertices().Count > 2);
}
[Test]
public void TestAddVertex()
{
double[] times = { 100, 700 };
float[] positions = { 200, 200 };
addBlueprintStep(times, positions, 0.2);
addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150);
addAddVertexSteps(90, 220);
addVertexCheckStep(4, 1, times[0], positions[0]);
addAddVertexSteps(750, 180);
addVertexCheckStep(5, 4, 750, 180);
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
}
[Test]
public void TestDeleteVertex()
{
double[] times = { 100, 300, 500 };
float[] positions = { 100, 200, 150 };
addBlueprintStep(times, positions);
addDeleteVertexSteps(times[1], positions[1]);
addVertexCheckStep(2, 1, times[2], positions[2]);
// The first vertex cannot be deleted.
addDeleteVertexSteps(times[0], positions[0]);
addVertexCheckStep(2, 0, times[0], positions[0]);
addDeleteVertexSteps(times[2], positions[2]);
addVertexCheckStep(1, 0, times[0], positions[0]);
}
[Test]
public void TestVertexResampling()
{
addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(100, 100),
new Vector2(50, 200),
}), 0.5);
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.PerfectCurve);
addAddVertexSteps(150, 150);
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.Linear);
}
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>
{
hitObject = new JuiceStream
{
StartTime = time,
X = x,
Path = sliderPath,
};
EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
EditorBeatmap.Add(hitObject);
EditorBeatmap.Update(hitObject);
Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject)); AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
});
private void addBlueprintStep(double[] times, float[] positions, double velocity = 0.5)
{
var path = new JuiceStreamPath();
for (int i = 1; i < times.Length; i++)
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
var sliderPath = new SliderPath();
path.ConvertToSliderPath(sliderPath, 0);
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
}
private IReadOnlyList<JuiceStreamPathVertex> getVertices() => this.ChildrenOfType<EditablePath>().Single().Vertices;
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
{
double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity;
float expectedX = x - hitObject.OriginalX;
var vertices = getVertices();
return vertices.Count == count &&
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
Precision.AlmostEquals(vertices[index].X, expectedX);
});
private void addDragStartStep(double time, float x)
{
AddMouseMoveStep(time, x);
AddStep("start dragging", () => InputManager.PressButton(MouseButton.Left));
}
private void addDragEndStep() => AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
private void addAddVertexSteps(double time, float x)
{
AddMouseMoveStep(time, x);
AddStep("add vertex", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
}
private void addDeleteVertexSteps(double time, float x)
{
AddMouseMoveStep(time, x);
AddStep("delete vertex", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ShiftLeft);
});
} }
} }
} }

View File

@ -3,7 +3,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -24,16 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public class TestSceneCatchSkinConfiguration : OsuTestScene public class TestSceneCatchSkinConfiguration : OsuTestScene
{ {
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
private Catcher catcher; private Catcher catcher;
private readonly Container container; private readonly Container container;
public TestSceneCatchSkinConfiguration() public TestSceneCatchSkinConfiguration()
{ {
Add(droppedObjectContainer = new DroppedObjectContainer());
Add(container = new Container { RelativeSizeAxes = Axes.Both }); Add(container = new Container { RelativeSizeAxes = Axes.Both });
} }
@ -46,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var skin = new TestSkin { FlipCatcherPlate = flip }; var skin = new TestSkin { FlipCatcherPlate = flip };
container.Child = new SkinProvidingContainer(skin) container.Child = new SkinProvidingContainer(skin)
{ {
Child = catcher = new Catcher(new Container()) Child = catcher = new Catcher(new Container(), new DroppedObjectContainer())
{ {
Anchor = Anchor.Centre Anchor = Anchor.Centre
} }

View File

@ -31,10 +31,12 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
private TestCatcher catcher; private Container trailContainer;
private DroppedObjectContainer droppedObjectContainer; private DroppedObjectContainer droppedObjectContainer;
private TestCatcher catcher;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
@ -43,24 +45,18 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = 0, CircleSize = 0,
}; };
var trailContainer = new Container trailContainer = new Container();
droppedObjectContainer = new DroppedObjectContainer();
Child = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
};
droppedObjectContainer = new DroppedObjectContainer();
Child = new DependencyProvidingContainer
{
CachedDependencies = new (Type, object)[]
{
(typeof(DroppedObjectContainer), droppedObjectContainer),
},
Children = new Drawable[] Children = new Drawable[]
{ {
droppedObjectContainer, droppedObjectContainer,
catcher = new TestCatcher(trailContainer, difficulty), catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty),
trailContainer trailContainer,
}, }
Anchor = Anchor.Centre
}; };
}); });
@ -298,8 +294,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>(); public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty) public TestCatcher(Container trailsTarget, DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty)
: base(trailsTarget, difficulty) : base(trailsTarget, droppedObjectTarget, difficulty)
{ {
} }
} }

View File

@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement()) area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement())
{ {
Type = area.MovableCatcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss Type = area.Catcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss
}); });
drawable.Expire(); drawable.Expire();
@ -119,16 +119,19 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea private class TestCatcherArea : CatcherArea
{ {
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
: base(beatmapDifficulty)
{ {
AddInternal(droppedObjectContainer = new DroppedObjectContainer()); var droppedObjectContainer = new DroppedObjectContainer();
Add(droppedObjectContainer);
Catcher = new Catcher(this, droppedObjectContainer, beatmapDifficulty)
{
X = CatchPlayfield.CENTER_X
};
} }
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1); public void ToggleHyperDash(bool status) => Catcher.SetHyperDashState(status ? 2 : 1);
} }
} }
} }

View File

@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive); private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive);
private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).CatcherArea.MovableCatcher.CurrentState; private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).Catcher.CurrentState;
private void spawnFruits(bool hit = false) private void spawnFruits(bool hit = false)
{ {
@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Catch.Tests
float xCoords = CatchPlayfield.CENTER_X; float xCoords = CatchPlayfield.CENTER_X;
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield) if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset; catchPlayfield.Catcher.X = xCoords - x_offset;
if (hit) if (hit)
xCoords -= x_offset; xCoords -= x_offset;

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests
// this needs to be done within the frame stable context due to how quickly hyperdash state changes occur. // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
Player.DrawableRuleset.FrameStableComponents.OnUpdate += d => Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
{ {
var catcher = Player.ChildrenOfType<CatcherArea>().FirstOrDefault()?.MovableCatcher; var catcher = Player.ChildrenOfType<Catcher>().FirstOrDefault();
if (catcher == null) if (catcher == null)
return; return;

View File

@ -113,36 +113,45 @@ namespace osu.Game.Rulesets.Catch.Tests
private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null) private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null)
{ {
CatcherArea catcherArea = null; Container trailsContainer = null;
Catcher catcher = null;
CatcherTrailDisplay trails = null; CatcherTrailDisplay trails = null;
AddStep("create hyper-dashing catcher", () => AddStep("create hyper-dashing catcher", () =>
{ {
Child = setupSkinHierarchy(catcherArea = new TestCatcherArea trailsContainer = new Container();
Child = setupSkinHierarchy(new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Children = new Drawable[]
{
catcher = new Catcher(trailsContainer, new DroppedObjectContainer())
{
Scale = new Vector2(4)
},
trailsContainer
}
}, skin); }, skin);
}); });
AddStep("get trails container", () => AddStep("get trails container", () =>
{ {
trails = catcherArea.OfType<CatcherTrailDisplay>().Single(); trails = trailsContainer.OfType<CatcherTrailDisplay>().Single();
catcherArea.MovableCatcher.SetHyperDashState(2); catcher.SetHyperDashState(2);
}); });
AddUntilStep("catcher colour is correct", () => catcherArea.MovableCatcher.Colour == expectedCatcherColour); AddUntilStep("catcher colour is correct", () => catcher.Colour == expectedCatcherColour);
AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour); AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour);
AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour)); AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour));
AddStep("finish hyper-dashing", () => AddStep("finish hyper-dashing", () =>
{ {
catcherArea.MovableCatcher.SetHyperDashState(); catcher.SetHyperDashState();
catcherArea.MovableCatcher.FinishTransforms(); catcher.FinishTransforms();
}); });
AddAssert("catcher colour returned to white", () => catcherArea.MovableCatcher.Colour == Color4.White); AddAssert("catcher colour returned to white", () => catcher.Colour == Color4.White);
} }
private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour) private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
@ -205,18 +214,5 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
} }
} }
private class TestCatcherArea : CatcherArea
{
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
public TestCatcherArea()
{
Scale = new Vector2(4f);
AddInternal(droppedObjectContainer = new DroppedObjectContainer());
}
}
} }
} }

View File

@ -0,0 +1,190 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public abstract class EditablePath : CompositeDrawable
{
public int PathId => path.InvalidationID;
public IReadOnlyList<JuiceStreamPathVertex> Vertices => path.Vertices;
public int VertexCount => path.Vertices.Count;
protected readonly Func<float, double> PositionToDistance;
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
private readonly JuiceStreamPath path = new JuiceStreamPath();
// Invariant: `path.Vertices.Count == vertexStates.Count`
private readonly List<VertexState> vertexStates = new List<VertexState>
{
new VertexState { IsFixed = true }
};
private readonly List<VertexState> previousVertexStates = new List<VertexState>();
[Resolved(CanBeNull = true)]
[CanBeNull]
private IBeatSnapProvider beatSnapProvider { get; set; }
protected EditablePath(Func<float, double> positionToDistance)
{
PositionToDistance = positionToDistance;
Anchor = Anchor.BottomLeft;
}
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{
while (path.Vertices.Count < InternalChildren.Count)
RemoveInternal(InternalChildren[^1]);
while (InternalChildren.Count < path.Vertices.Count)
AddInternal(new VertexPiece());
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
for (int i = 0; i < VertexCount; i++)
{
var piece = (VertexPiece)InternalChildren[i];
var vertex = path.Vertices[i];
piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor));
piece.UpdateFrom(vertexStates[i]);
}
}
public void InitializeFromHitObject(JuiceStream hitObject)
{
var sliderPath = hitObject.Path;
path.ConvertFromSliderPath(sliderPath);
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
if (sliderPath.ControlPoints.Any(p => p.Type.Value != null && p.Type.Value != PathType.Linear))
{
path.ResampleVertices(hitObject.NestedHitObjects
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
.Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity));
}
vertexStates.Clear();
vertexStates.AddRange(path.Vertices.Select((_, i) => new VertexState
{
IsFixed = i == 0
}));
}
public void UpdateHitObjectFromPath(JuiceStream hitObject)
{
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
if (beatSnapProvider == null) return;
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
}
public Vector2 ToRelativePosition(Vector2 screenSpacePosition)
{
return ToLocalSpace(screenSpacePosition) - new Vector2(0, DrawHeight);
}
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
protected int AddVertex(double distance, float x)
{
int index = path.InsertVertex(distance);
path.SetVertexPosition(index, x);
vertexStates.Insert(index, new VertexState());
correctFixedVertexPositions();
Debug.Assert(vertexStates.Count == VertexCount);
return index;
}
protected bool RemoveVertex(int index)
{
if (index < 0 || index >= path.Vertices.Count)
return false;
if (vertexStates[index].IsFixed)
return false;
path.RemoveVertices((_, i) => i == index);
vertexStates.RemoveAt(index);
if (vertexStates.Count == 0)
vertexStates.Add(new VertexState());
Debug.Assert(vertexStates.Count == VertexCount);
return true;
}
protected void MoveSelectedVertices(double distanceDelta, float xDelta)
{
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
previousVertexStates.Clear();
previousVertexStates.AddRange(vertexStates);
// We will recreate the path from scratch. Note that `Clear` leaves the first vertex.
int vertexCount = VertexCount;
path.Clear();
vertexStates.RemoveRange(1, vertexCount - 1);
for (int i = 1; i < vertexCount; i++)
{
var state = previousVertexStates[i];
double distance = state.VertexBeforeChange.Distance;
if (state.IsSelected)
distance += distanceDelta;
int newIndex = path.InsertVertex(Math.Max(0, distance));
vertexStates.Insert(newIndex, state);
}
// First, restore positions of the non-selected vertices.
for (int i = 0; i < vertexCount; i++)
{
if (!vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
}
// Then, move the selected vertices.
for (int i = 0; i < vertexCount; i++)
{
if (vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X + xDelta);
}
// Finally, correct the position of fixed vertices.
correctFixedVertexPositions();
}
private void correctFixedVertexPositions()
{
for (int i = 0; i < VertexCount; i++)
{
if (vertexStates[i].IsFixed)
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
}
}
}
}

View File

@ -0,0 +1,130 @@
// 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.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class SelectionEditablePath : EditablePath, IHasContextMenu
{
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
// To handle when the editor is scrolled while dragging.
private Vector2 dragStartPosition;
[Resolved(CanBeNull = true)]
[CanBeNull]
private IEditorChangeHandler changeHandler { get; set; }
public SelectionEditablePath(Func<float, double> positionToDistance)
: base(positionToDistance)
{
}
public void AddVertex(Vector2 relativePosition)
{
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
int index = AddVertex(distance, relativePosition.X);
selectOnly(index);
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
protected override bool OnMouseDown(MouseDownEvent e)
{
int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
if (index == -1 || VertexStates[index].IsFixed)
return false;
if (e.Button == MouseButton.Left && e.ShiftPressed)
{
RemoveVertex(index);
return true;
}
if (e.ControlPressed)
VertexStates[index].IsSelected = !VertexStates[index].IsSelected;
else if (!VertexStates[index].IsSelected)
selectOnly(index);
// Don't inhibit right click, to show the context menu
return e.Button != MouseButton.Right;
}
protected override bool OnDragStart(DragStartEvent e)
{
int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
if (index == -1 || VertexStates[index].IsFixed)
return false;
if (e.Button != MouseButton.Left)
return false;
dragStartPosition = ToRelativePosition(e.ScreenSpaceMouseDownPosition);
for (int i = 0; i < VertexCount; i++)
VertexStates[i].VertexBeforeChange = Vertices[i];
changeHandler?.BeginChange();
return true;
}
protected override void OnDrag(DragEvent e)
{
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
float xDelta = mousePosition.X - dragStartPosition.X;
MoveSelectedVertices(distanceDelta, xDelta);
}
protected override void OnDragEnd(DragEndEvent e)
{
changeHandler?.EndChange();
}
private int getMouseTargetVertex(Vector2 screenSpacePosition)
{
for (int i = InternalChildren.Count - 1; i >= 0; i--)
{
if (i < VertexCount && InternalChildren[i].ReceivePositionalInputAt(screenSpacePosition))
return i;
}
return -1;
}
private IEnumerable<MenuItem> getContextMenuItems()
{
int selectedCount = VertexStates.Count(state => state.IsSelected);
if (selectedCount != 0)
yield return new OsuMenuItem($"Delete selected {(selectedCount == 1 ? "vertex" : $"{selectedCount} vertices")}", MenuItemType.Destructive, deleteSelectedVertices);
}
private void selectOnly(int index)
{
for (int i = 0; i < VertexCount; i++)
VertexStates[i].IsSelected = i == index;
}
private void deleteSelectedVertices()
{
for (int i = VertexCount - 1; i >= 0; i--)
{
if (VertexStates[i].IsSelected)
RemoveVertex(i);
}
}
}
}

View File

@ -0,0 +1,31 @@
// 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.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class VertexPiece : Circle
{
[Resolved]
private OsuColour osuColour { get; set; }
public VertexPiece()
{
Anchor = Anchor.BottomLeft;
Origin = Anchor.Centre;
Size = new Vector2(15);
}
public void UpdateFrom(VertexState state)
{
Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow;
Alpha = state.IsFixed ? 0.5f : 1;
}
}
}

View File

@ -0,0 +1,31 @@
// 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.Rulesets.Catch.Objects;
#nullable enable
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
/// <summary>
/// Holds the state of a vertex in the path of a <see cref="EditablePath"/>.
/// </summary>
public class VertexState
{
/// <summary>
/// Whether the vertex is selected.
/// </summary>
public bool IsSelected { get; set; }
/// <summary>
/// Whether the vertex can be moved or deleted.
/// </summary>
public bool IsFixed { get; set; }
/// <summary>
/// The position of the vertex before a vertex moving operation starts.
/// This is used to implement "memory-less" moving operations (only the final position matters) to improve UX.
/// </summary>
public JuiceStreamPathVertex VertexBeforeChange { get; set; }
}
}

View File

@ -1,15 +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 System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
@ -17,6 +24,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight))); public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
public override MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
private float minNestedX; private float minNestedX;
private float maxNestedX; private float maxNestedX;
@ -26,13 +35,34 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private readonly Cached pathCache = new Cached(); private readonly Cached pathCache = new Cached();
private readonly SelectionEditablePath editablePath;
/// <summary>
/// The <see cref="JuiceStreamPath.InvalidationID"/> of the <see cref="JuiceStreamPath"/> corresponding the current <see cref="SliderPath"/> of the hit object.
/// When the path is edited, the change is detected and the <see cref="SliderPath"/> of the hit object is updated.
/// </summary>
private int lastEditablePathId = -1;
/// <summary>
/// The <see cref="SliderPath.Version"/> of the current <see cref="SliderPath"/> of the hit object.
/// When the <see cref="SliderPath"/> of the hit object is changed by external means, the change is detected and the <see cref="JuiceStreamPath"/> is re-initialized.
/// </summary>
private int lastSliderPathVersion = -1;
private Vector2 rightMouseDownPosition;
[Resolved(CanBeNull = true)]
[CanBeNull]
private EditorBeatmap editorBeatmap { get; set; }
public JuiceStreamSelectionBlueprint(JuiceStream hitObject) public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
: base(hitObject) : base(hitObject)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
scrollingPath = new ScrollingPath(), scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer() nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new SelectionEditablePath(positionToDistance)
}; };
} }
@ -49,7 +79,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
if (!IsSelected) return; if (!IsSelected) return;
nestedOutlineContainer.Position = scrollingPath.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject); if (editablePath.PathId != lastEditablePathId)
updateHitObjectFromPath();
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
editablePath.UpdateFrom(HitObjectContainer, HitObject);
if (pathCache.IsValid) return; if (pathCache.IsValid) return;
@ -59,10 +95,38 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
pathCache.Validate(); pathCache.Validate();
} }
protected override void OnSelected()
{
initializeJuiceStreamPath();
base.OnSelected();
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!IsSelected) return base.OnMouseDown(e);
switch (e.Button)
{
case MouseButton.Left when e.ControlPressed:
editablePath.AddVertex(editablePath.ToRelativePosition(e.ScreenSpaceMouseDownPosition));
return true;
case MouseButton.Right:
// Record the mouse position to be used in the "add vertex" action.
rightMouseDownPosition = editablePath.ToRelativePosition(e.ScreenSpaceMouseDownPosition);
break;
}
return base.OnMouseDown(e);
}
private void onDefaultsApplied(HitObject _) private void onDefaultsApplied(HitObject _)
{ {
computeObjectBounds(); computeObjectBounds();
pathCache.Invalidate(); pathCache.Invalidate();
if (lastSliderPathVersion != HitObject.Path.Version.Value)
initializeJuiceStreamPath();
} }
private void computeObjectBounds() private void computeObjectBounds()
@ -81,6 +145,38 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius); return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
} }
private double positionToDistance(float relativeYPosition)
{
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity;
}
private void initializeJuiceStreamPath()
{
editablePath.InitializeFromHitObject(HitObject);
// Record the current ID to update the hit object only when a change is made to the path.
lastEditablePathId = editablePath.PathId;
lastSliderPathVersion = HitObject.Path.Version.Value;
}
private void updateHitObjectFromPath()
{
editablePath.UpdateHitObjectFromPath(HitObject);
editorBeatmap?.Update(HitObject);
lastEditablePathId = editablePath.PathId;
lastSliderPathVersion = HitObject.Path.Version.Value;
}
private IEnumerable<MenuItem> getContextMenuItems()
{
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
{
editablePath.AddVertex(rightMouseDownPosition);
});
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit
base.LoadComplete(); base.LoadComplete();
// TODO: honor "hit animation" setting? // TODO: honor "hit animation" setting?
CatcherArea.MovableCatcher.CatchFruitOnPlate = false; Catcher.CatchFruitOnPlate = false;
// TODO: disable hit lighting as well // TODO: disable hit lighting as well
} }

View File

@ -1,7 +1,10 @@
// 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 osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK; using osuTK;
@ -20,5 +23,41 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
return new Vector2(hitObject.OriginalX, hitObjectContainer.PositionAtTime(hitObject.StartTime)); return new Vector2(hitObject.OriginalX, hitObjectContainer.PositionAtTime(hitObject.StartTime));
} }
/// <summary>
/// Get the range of horizontal position occupied by the hit object.
/// </summary>
/// <remarks>
/// <see cref="TinyDroplet"/>s are excluded and returns <see cref="PositionRange.EMPTY"/>.
/// </remarks>
public static PositionRange GetPositionRange(HitObject hitObject)
{
switch (hitObject)
{
case Fruit fruit:
return new PositionRange(fruit.OriginalX);
case Droplet droplet:
return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX);
case JuiceStream _:
return GetPositionRange(hitObject.NestedHitObjects);
case BananaShower _:
// A banana shower occupies the whole screen width.
return new PositionRange(0, CatchPlayfield.WIDTH);
default:
return PositionRange.EMPTY;
}
}
/// <summary>
/// Get the range of horizontal position occupied by the hit objects.
/// </summary>
/// <remarks>
/// <see cref="TinyDroplet"/>s are excluded.
/// </remarks>
public static PositionRange GetPositionRange(IEnumerable<HitObject> hitObjects) => hitObjects.Select(GetPositionRange).Aggregate(PositionRange.EMPTY, PositionRange.Union);
} }
} }

View File

@ -12,6 +12,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
using Direction = osu.Framework.Graphics.Direction;
namespace osu.Game.Rulesets.Catch.Edit namespace osu.Game.Rulesets.Catch.Edit
{ {
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit
Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta); Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
float deltaX = targetPosition.X - originalPosition.X; float deltaX = targetPosition.X - originalPosition.X;
deltaX = limitMovement(deltaX, EditorBeatmap.SelectedHitObjects); deltaX = limitMovement(deltaX, SelectedItems);
if (deltaX == 0) if (deltaX == 0)
{ {
@ -39,18 +40,60 @@ namespace osu.Game.Rulesets.Catch.Edit
EditorBeatmap.PerformOnSelection(h => EditorBeatmap.PerformOnSelection(h =>
{ {
if (!(h is CatchHitObject hitObject)) return; if (!(h is CatchHitObject catchObject)) return;
hitObject.OriginalX += deltaX; catchObject.OriginalX += deltaX;
// Move the nested hit objects to give an instant result before nested objects are recreated. // Move the nested hit objects to give an instant result before nested objects are recreated.
foreach (var nested in hitObject.NestedHitObjects.OfType<CatchHitObject>()) foreach (var nested in catchObject.NestedHitObjects.OfType<CatchHitObject>())
nested.OriginalX += deltaX; nested.OriginalX += deltaX;
}); });
return true; return true;
} }
public override bool HandleFlip(Direction direction)
{
var selectionRange = CatchHitObjectUtils.GetPositionRange(SelectedItems);
bool changed = false;
EditorBeatmap.PerformOnSelection(h =>
{
if (h is CatchHitObject catchObject)
changed |= handleFlip(selectionRange, catchObject);
});
return changed;
}
public override bool HandleReverse()
{
double selectionStartTime = SelectedItems.Min(h => h.StartTime);
double selectionEndTime = SelectedItems.Max(h => h.GetEndTime());
EditorBeatmap.PerformOnSelection(hitObject =>
{
hitObject.StartTime = selectionEndTime - (hitObject.GetEndTime() - selectionStartTime);
if (hitObject is JuiceStream juiceStream)
{
juiceStream.Path.Reverse(out Vector2 positionalOffset);
juiceStream.OriginalX += positionalOffset.X;
juiceStream.LegacyConvertedY += positionalOffset.Y;
EditorBeatmap.Update(juiceStream);
}
});
return true;
}
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
var selectionRange = CatchHitObjectUtils.GetPositionRange(SelectedItems);
SelectionBox.CanFlipX = selectionRange.Length > 0 && SelectedItems.Any(h => h is CatchHitObject && !(h is BananaShower));
SelectionBox.CanReverse = SelectedItems.Count > 1 || SelectedItems.Any(h => h is JuiceStream);
}
/// <summary> /// <summary>
/// Limit positional movement of the objects by the constraint that moved objects should stay in bounds. /// Limit positional movement of the objects by the constraint that moved objects should stay in bounds.
/// </summary> /// </summary>
@ -59,20 +102,12 @@ namespace osu.Game.Rulesets.Catch.Edit
/// <returns>The positional movement with the restriction applied.</returns> /// <returns>The positional movement with the restriction applied.</returns>
private float limitMovement(float deltaX, IEnumerable<HitObject> movingObjects) private float limitMovement(float deltaX, IEnumerable<HitObject> movingObjects)
{ {
float minX = float.PositiveInfinity; var range = CatchHitObjectUtils.GetPositionRange(movingObjects);
float maxX = float.NegativeInfinity;
foreach (float x in movingObjects.SelectMany(getOriginalPositions))
{
minX = Math.Min(minX, x);
maxX = Math.Max(maxX, x);
}
// To make an object with position `x` stay in bounds after `deltaX` movement, `0 <= x + deltaX <= WIDTH` should be satisfied. // To make an object with position `x` stay in bounds after `deltaX` movement, `0 <= x + deltaX <= WIDTH` should be satisfied.
// Subtracting `x`, we get `-x <= deltaX <= WIDTH - x`. // Subtracting `x`, we get `-x <= deltaX <= WIDTH - x`.
// We only need to apply the inequality to extreme values of `x`. // We only need to apply the inequality to extreme values of `x`.
float lowerBound = -minX; float lowerBound = -range.Min;
float upperBound = CatchPlayfield.WIDTH - maxX; float upperBound = CatchPlayfield.WIDTH - range.Max;
// The inequality may be unsatisfiable if the objects were already out of bounds. // The inequality may be unsatisfiable if the objects were already out of bounds.
// In that case, don't move objects at all. // In that case, don't move objects at all.
if (lowerBound > upperBound) if (lowerBound > upperBound)
@ -81,35 +116,25 @@ namespace osu.Game.Rulesets.Catch.Edit
return Math.Clamp(deltaX, lowerBound, upperBound); return Math.Clamp(deltaX, lowerBound, upperBound);
} }
/// <summary> private bool handleFlip(PositionRange selectionRange, CatchHitObject hitObject)
/// Enumerate X positions that should be contained in-bounds after move offset is applied.
/// </summary>
private IEnumerable<float> getOriginalPositions(HitObject hitObject)
{ {
switch (hitObject) switch (hitObject)
{ {
case Fruit fruit: case BananaShower _:
yield return fruit.OriginalX; return false;
break;
case JuiceStream juiceStream: case JuiceStream juiceStream:
foreach (var nested in juiceStream.NestedHitObjects.OfType<CatchHitObject>()) juiceStream.OriginalX = selectionRange.GetFlippedPosition(juiceStream.OriginalX);
{
// Even if `OriginalX` is outside the playfield, tiny droplets can be moved inside the playfield after the random offset application.
if (!(nested is TinyDroplet))
yield return nested.OriginalX;
}
break; foreach (var point in juiceStream.Path.ControlPoints)
point.Position.Value *= new Vector2(-1, 1);
case BananaShower _: EditorBeatmap.Update(juiceStream);
// A banana shower occupies the whole screen width. return true;
// If the selection contains a banana shower, the selection cannot be moved horizontally.
yield return 0;
yield return CatchPlayfield.WIDTH;
break; default:
hitObject.OriginalX = selectionRange.GetFlippedPosition(hitObject.OriginalX);
return true;
} }
} }
} }

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 System;
#nullable enable
namespace osu.Game.Rulesets.Catch.Edit
{
/// <summary>
/// Represents either the empty range or a closed interval of horizontal positions in the playfield.
/// A <see cref="PositionRange"/> represents a closed interval if it is <see cref="Min"/> &lt;= <see cref="Max"/>, and represents the empty range otherwise.
/// </summary>
public readonly struct PositionRange
{
public readonly float Min;
public readonly float Max;
public float Length => Math.Max(0, Max - Min);
public PositionRange(float value)
: this(value, value)
{
}
public PositionRange(float min, float max)
{
Min = min;
Max = max;
}
public static PositionRange Union(PositionRange a, PositionRange b) => new PositionRange(Math.Min(a.Min, b.Min), Math.Max(a.Max, b.Max));
/// <summary>
/// Get the given position flipped (mirrored) for the axis at the center of this range.
/// Returns the given position unchanged if the range was empty.
/// </summary>
public float GetFlippedPosition(float x) => Min <= Max ? Max - (x - Min) : x;
public static readonly PositionRange EMPTY = new PositionRange(float.PositiveInfinity, float.NegativeInfinity);
}
}

View File

@ -41,9 +41,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
base.Update(); base.Update();
var catcherArea = playfield.CatcherArea; FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this);
FlashlightPosition = catcherArea.ToSpaceOfOtherDrawable(catcherArea.MovableCatcher.DrawPosition, this);
} }
private float getSizeFor(int combo) private float getSizeFor(int combo)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Mods
var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset; var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset;
var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield; var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield;
catchPlayfield.CatcherArea.MovableCatcher.CatchFruitOnPlate = false; catchPlayfield.Catcher.CatchFruitOnPlate = false;
} }
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)

View File

@ -9,6 +9,7 @@ using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK.Graphics; using osuTK.Graphics;
@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
// override any external colour changes with banananana // override any external colour changes with banananana
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => getBananaColour(); Color4 IHasComboInformation.GetComboColour(ISkin skin) => getBananaColour();
private Color4 getBananaColour() private Color4 getBananaColour()
{ {

View File

@ -1,10 +1,10 @@
// 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 Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
@ -45,6 +45,6 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
} }
Color4 IHasComboInformation.GetComboColour(IReadOnlyList<Color4> comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count]; Color4 IHasComboInformation.GetComboColour(ISkin skin) => IHasComboInformation.GetSkinComboColour(this, skin, IndexInBeatmap + 1);
} }
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Replays
bool impossibleJump = speedRequired > movement_speed * 2; bool impossibleJump = speedRequired > movement_speed * 2;
// todo: get correct catcher size, based on difficulty CS. // todo: get correct catcher size, based on difficulty CS.
const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f; const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX) if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
{ {

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
@ -26,38 +27,53 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
public const float CENTER_X = WIDTH / 2; public const float CENTER_X = WIDTH / 2;
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
internal readonly CatcherArea CatcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
// only check the X position; handle all vertical space. // only check the X position; handle all vertical space.
base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y)); base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y));
internal Catcher Catcher { get; private set; }
internal CatcherArea CatcherArea { get; private set; }
private readonly BeatmapDifficulty difficulty;
public CatchPlayfield(BeatmapDifficulty difficulty) public CatchPlayfield(BeatmapDifficulty difficulty)
{ {
CatcherArea = new CatcherArea(difficulty) this.difficulty = difficulty;
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
};
InternalChildren = new[]
{
droppedObjectContainer = new DroppedObjectContainer(),
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer.CreateProxy(),
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
// make sure the up-to-date catcher position is used for the catcher catching logic of hit objects.
CatcherArea,
HitObjectContainer,
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var trailContainer = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft
};
var droppedObjectContainer = new DroppedObjectContainer();
Catcher = new Catcher(trailContainer, droppedObjectContainer, difficulty)
{
X = CENTER_X
};
AddRangeInternal(new[]
{
droppedObjectContainer,
Catcher.CreateProxiedContent(),
HitObjectContainer.CreateProxy(),
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
// make sure the up-to-date catcher position is used for the catcher catching logic of hit objects.
CatcherArea = new CatcherArea
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
Catcher = Catcher,
},
trailContainer,
HitObjectContainer,
});
RegisterPool<Droplet, DrawableDroplet>(50); RegisterPool<Droplet, DrawableDroplet>(50);
RegisterPool<TinyDroplet, DrawableTinyDroplet>(50); RegisterPool<TinyDroplet, DrawableTinyDroplet>(50);
RegisterPool<Fruit, DrawableFruit>(100); RegisterPool<Fruit, DrawableFruit>(100);
@ -80,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.UI
((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch; ((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch;
} }
private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.MovableCatcher.CanCatch(obj); private bool checkIfWeCanCatch(CatchHitObject obj) => Catcher.CanCatch(obj);
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);

View File

@ -21,6 +21,6 @@ namespace osu.Game.Rulesets.Catch.UI
} }
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<CatchAction> actions, ReplayFrame previousFrame) protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<CatchAction> actions, ReplayFrame previousFrame)
=> new CatchReplayFrame(Time.Current, playfield.CatcherArea.MovableCatcher.X, actions.Contains(CatchAction.Dash), previousFrame as CatchReplayFrame); => new CatchReplayFrame(Time.Current, playfield.Catcher.X, actions.Contains(CatchAction.Dash), previousFrame as CatchReplayFrame);
} }
} }

View File

@ -25,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
public class Catcher : SkinReloadableDrawable public class Catcher : SkinReloadableDrawable
{ {
/// <summary>
/// The size of the catcher at 1x scale.
/// </summary>
public const float BASE_SIZE = 106.75f;
/// <summary>
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
/// </summary>
public const float ALLOWED_CATCH_RANGE = 0.8f;
/// <summary> /// <summary>
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail /// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail
/// and end glow/after-image during a hyper-dash. /// and end glow/after-image during a hyper-dash.
@ -74,8 +84,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary> /// <summary>
/// Contains objects dropped from the plate. /// Contains objects dropped from the plate.
/// </summary> /// </summary>
[Resolved] private readonly DroppedObjectContainer droppedObjectTarget;
private DroppedObjectContainer droppedObjectTarget { get; set; }
public CatcherAnimationState CurrentState public CatcherAnimationState CurrentState
{ {
@ -83,11 +92,6 @@ namespace osu.Game.Rulesets.Catch.UI
private set => Body.AnimationState.Value = value; private set => Body.AnimationState.Value = value;
} }
/// <summary>
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
/// </summary>
public const float ALLOWED_CATCH_RANGE = 0.8f;
private bool dashing; private bool dashing;
public bool Dashing public bool Dashing
@ -134,13 +138,14 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool<CaughtBanana> caughtBananaPool; private readonly DrawablePool<CaughtBanana> caughtBananaPool;
private readonly DrawablePool<CaughtDroplet> caughtDropletPool; private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) public Catcher([NotNull] Container trailsTarget, [NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null)
{ {
this.trailsTarget = trailsTarget; this.trailsTarget = trailsTarget;
this.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
Size = new Vector2(CatcherArea.CATCHER_SIZE); Size = new Vector2(BASE_SIZE);
if (difficulty != null) if (difficulty != null)
Scale = calculateScale(difficulty); Scale = calculateScale(difficulty);
@ -197,7 +202,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// Calculates the width of the area used for attempting catches in gameplay. /// Calculates the width of the area used for attempting catches in gameplay.
/// </summary> /// </summary>
/// <param name="scale">The scale of the catcher.</param> /// <param name="scale">The scale of the catcher.</param>
public static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE; public static float CalculateCatchWidth(Vector2 scale) => BASE_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE;
/// <summary> /// <summary>
/// Calculates the width of the area used for attempting catches in gameplay. /// Calculates the width of the area used for attempting catches in gameplay.

View File

@ -5,7 +5,6 @@ using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
@ -16,13 +15,29 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
/// <summary>
/// The horizontal band at the bottom of the playfield the catcher is moving on.
/// It holds a <see cref="Catcher"/> as a child and translates input to the catcher movement.
/// It also holds a combo display that is above the catcher, and judgment results are translated to the catcher and the combo display.
/// </summary>
public class CatcherArea : Container, IKeyBindingHandler<CatchAction> public class CatcherArea : Container, IKeyBindingHandler<CatchAction>
{ {
public const float CATCHER_SIZE = 106.75f; public Catcher Catcher
{
get => catcher;
set
{
if (catcher != null)
Remove(catcher);
Add(catcher = value);
}
}
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay; private readonly CatchComboDisplay comboDisplay;
private Catcher catcher;
/// <summary> /// <summary>
/// <c>-1</c> when only left button is pressed. /// <c>-1</c> when only left button is pressed.
/// <c>1</c> when only right button is pressed. /// <c>1</c> when only right button is pressed.
@ -30,27 +45,26 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
private int currentDirection; private int currentDirection;
public CatcherArea(BeatmapDifficulty difficulty = null) /// <remarks>
/// <see cref="Catcher"/> must be set before loading.
/// </remarks>
public CatcherArea()
{ {
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); Size = new Vector2(CatchPlayfield.WIDTH, Catcher.BASE_SIZE);
Children = new Drawable[] Child = comboDisplay = new CatchComboDisplay
{ {
comboDisplay = new CatchComboDisplay RelativeSizeAxes = Axes.None,
{ AutoSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.None, Anchor = Anchor.TopLeft,
AutoSizeAxes = Axes.Both, Origin = Anchor.Centre,
Anchor = Anchor.TopLeft, Margin = new MarginPadding { Bottom = 350f },
Origin = Anchor.Centre, X = CatchPlayfield.CENTER_X
Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X
},
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
}; };
} }
public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result) public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result)
{ {
MovableCatcher.OnNewResult(hitObject, result); Catcher.OnNewResult(hitObject, result);
if (!result.Type.IsScorable()) if (!result.Type.IsScorable())
return; return;
@ -58,9 +72,9 @@ namespace osu.Game.Rulesets.Catch.UI
if (hitObject.HitObject.LastInCombo) if (hitObject.HitObject.LastInCombo)
{ {
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result)) if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
MovableCatcher.Explode(); Catcher.Explode();
else else
MovableCatcher.Drop(); Catcher.Drop();
} }
comboDisplay.OnNewResult(hitObject, result); comboDisplay.OnNewResult(hitObject, result);
@ -69,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.UI
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result) public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result)
{ {
comboDisplay.OnRevertResult(hitObject, result); comboDisplay.OnRevertResult(hitObject, result);
MovableCatcher.OnRevertResult(hitObject, result); Catcher.OnRevertResult(hitObject, result);
} }
protected override void Update() protected override void Update()
@ -80,27 +94,27 @@ namespace osu.Game.Rulesets.Catch.UI
SetCatcherPosition( SetCatcherPosition(
replayState?.CatcherX ?? replayState?.CatcherX ??
(float)(MovableCatcher.X + MovableCatcher.Speed * currentDirection * Clock.ElapsedFrameTime)); (float)(Catcher.X + Catcher.Speed * currentDirection * Clock.ElapsedFrameTime));
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
comboDisplay.X = MovableCatcher.X; comboDisplay.X = Catcher.X;
} }
public void SetCatcherPosition(float X) public void SetCatcherPosition(float X)
{ {
float lastPosition = MovableCatcher.X; float lastPosition = Catcher.X;
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH); float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
MovableCatcher.X = newPosition; Catcher.X = newPosition;
if (lastPosition < newPosition) if (lastPosition < newPosition)
MovableCatcher.VisualDirection = Direction.Right; Catcher.VisualDirection = Direction.Right;
else if (lastPosition > newPosition) else if (lastPosition > newPosition)
MovableCatcher.VisualDirection = Direction.Left; Catcher.VisualDirection = Direction.Left;
} }
public bool OnPressed(CatchAction action) public bool OnPressed(CatchAction action)
@ -116,7 +130,7 @@ namespace osu.Game.Rulesets.Catch.UI
return true; return true;
case CatchAction.Dash: case CatchAction.Dash:
MovableCatcher.Dashing = true; Catcher.Dashing = true;
return true; return true;
} }
@ -136,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.UI
break; break;
case CatchAction.Dash: case CatchAction.Dash:
MovableCatcher.Dashing = false; Catcher.Dashing = false;
break; break;
} }
} }

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherTrail() public CatcherTrail()
{ {
Size = new Vector2(CatcherArea.CATCHER_SIZE); Size = new Vector2(Catcher.BASE_SIZE);
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
Blending = BlendingParameters.Additive; Blending = BlendingParameters.Additive;
InternalChild = body = new SkinnableCatcher InternalChild = body = new SkinnableCatcher

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
Anchor = Anchor.TopCentre; Anchor = Anchor.TopCentre;
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE; OriginPosition = new Vector2(0.5f, 0.06f) * Catcher.BASE_SIZE;
} }
} }
} }

View File

@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.9311451172574934d, "diffcalc-test")] [TestCase(6.7568168283591499d, "diffcalc-test")]
[TestCase(1.0736586907780401d, "zero-length-sliders")] [TestCase(1.0348244046058293d, "zero-length-sliders")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(8.7212283220412345d, "diffcalc-test")] [TestCase(8.4783236764532557d, "diffcalc-test")]
[TestCase(1.3212137158641493d, "zero-length-sliders")] [TestCase(1.2708532136987165d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime()); => Test(expected, name, new OsuModDoubleTime());

View File

@ -103,22 +103,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400)))); double approachRateTotalHitsFactor = 1.0 / (1.0 + Math.Exp(-(0.007 * (totalHits - 400))));
aimValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden)) if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
double flashlightBonus = 1.0;
if (mods.Any(h => h is OsuModFlashlight)) if (mods.Any(h => h is OsuModFlashlight))
{ {
// Apply object-based bonus for flashlight. // Apply object-based bonus for flashlight.
aimValue *= 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) + flashlightBonus = 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
(totalHits > 200 (totalHits > 200
? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) + ? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
(totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0) (totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
: 0.0); : 0.0);
} }
aimValue *= Math.Max(flashlightBonus, approachRateBonus);
// Scale the aim value with accuracy _slightly_ // Scale the aim value with accuracy _slightly_
aimValue *= 0.5 + accuracy / 2.0; aimValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that // It is important to also consider accuracy difficulty when doing that

View File

@ -3,7 +3,6 @@
using System; using System;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// <summary> /// <summary>
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances. /// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
/// </summary> /// </summary>
public class Aim : StrainSkill public class Aim : OsuStrainSkill
{ {
private const double angle_bonus_begin = Math.PI / 3; private const double angle_bonus_begin = Math.PI / 3;
private const double timing_threshold = 107; private const double timing_threshold = 107;
@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
Math.Max(osuPrevious.JumpDistance - scale, 0) Math.Max(osuPrevious.JumpDistance - scale, 0)
* Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2) * Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2)
* Math.Max(osuCurrent.JumpDistance - scale, 0)); * Math.Max(osuCurrent.JumpDistance - scale, 0));
result = 1.5 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime); result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime);
} }
} }

View File

@ -0,0 +1,61 @@
// 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.Collections.Generic;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using System.Linq;
using osu.Framework.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
public abstract class OsuStrainSkill : StrainSkill
{
/// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
/// </summary>
protected virtual int ReducedSectionCount => 10;
/// <summary>
/// The baseline multiplier applied to the section with the biggest strain.
/// </summary>
protected virtual double ReducedStrainBaseline => 0.75;
/// <summary>
/// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations.
/// </summary>
protected virtual double DifficultyMultiplier => 1.06;
protected OsuStrainSkill(Mod[] mods)
: base(mods)
{
}
public override double DifficultyValue()
{
double difficulty = 0;
double weight = 1;
List<double> strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList();
// We are reducing the highest strains first to account for extreme difficulty spikes
for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)
{
double scale = Math.Log10(Interpolation.Lerp(1, 10, Math.Clamp((float)i / ReducedSectionCount, 0, 1)));
strains[i] *= Interpolation.Lerp(ReducedStrainBaseline, 1.0, scale);
}
// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
foreach (double strain in strains.OrderByDescending(d => d))
{
difficulty += strain * weight;
weight *= DecayWeight;
}
return difficulty * DifficultyMultiplier;
}
}
}

View File

@ -3,7 +3,6 @@
using System; using System;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -13,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// <summary> /// <summary>
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit. /// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
/// </summary> /// </summary>
public class Speed : StrainSkill public class Speed : OsuStrainSkill
{ {
private const double single_spacing_threshold = 125; private const double single_spacing_threshold = 125;
@ -23,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double SkillMultiplier => 1400; protected override double SkillMultiplier => 1400;
protected override double StrainDecayBase => 0.3; protected override double StrainDecayBase => 0.3;
protected override int ReducedSectionCount => 5;
protected override double DifficultyMultiplier => 1.04;
private const double min_speed_bonus = 75; // ~200BPM private const double min_speed_bonus = 75; // ~200BPM
private const double max_speed_bonus = 45; // ~330BPM private const double max_speed_bonus = 45; // ~330BPM

View File

@ -129,9 +129,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public bool OnPressed(PlatformAction action) public bool OnPressed(PlatformAction action)
{ {
switch (action.ActionMethod) switch (action)
{ {
case PlatformActionMethod.Delete: case PlatformAction.Delete:
return DeleteSelected(); return DeleteSelected();
} }

View File

@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Osu.Edit
Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad(); Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0; SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
SelectionBox.CanScaleY = quad.Height > 0; SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
} }
@ -76,32 +76,8 @@ namespace osu.Game.Rulesets.Osu.Edit
if (h is Slider slider) if (h is Slider slider)
{ {
var points = slider.Path.ControlPoints.ToArray(); slider.Path.Reverse(out Vector2 offset);
Vector2 endPos = points.Last().Position.Value; slider.Position += offset;
slider.Path.ControlPoints.Clear();
slider.Position += endPos;
PathType? lastType = null;
for (var i = 0; i < points.Length; i++)
{
var p = points[i];
p.Position.Value -= endPos;
// propagate types forwards to last null type
if (i == points.Length - 1)
p.Type.Value = lastType;
else if (p.Type.Value != null)
{
var newType = p.Type.Value;
p.Type.Value = lastType;
lastType = newType;
}
slider.Path.ControlPoints.Insert(0, p);
}
} }
} }

View File

@ -129,14 +129,8 @@ namespace osu.Game.Tests.Gameplay
{ {
switch (lookup) switch (lookup)
{ {
case GlobalSkinColours global: case SkinComboColourLookup comboColour:
switch (global) return SkinUtils.As<TValue>(new Bindable<Color4>(ComboColours[comboColour.ColourIndex % ComboColours.Count]));
{
case GlobalSkinColours.ComboColours:
return SkinUtils.As<TValue>(new Bindable<IReadOnlyList<Color4>>(ComboColours));
}
break;
} }
throw new NotImplementedException(); throw new NotImplementedException();

View File

@ -35,6 +35,8 @@ namespace osu.Game.Tests.Visual.Editing
CanRotate = true, CanRotate = true,
CanScaleX = true, CanScaleX = true,
CanScaleY = true, CanScaleY = true,
CanFlipX = true,
CanFlipY = true,
OnRotation = handleRotation, OnRotation = handleRotation,
OnScale = handleScale OnScale = handleScale

View File

@ -104,6 +104,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
} }
[Test]
public void TestExitMidJoin()
{
Room room = null;
AddStep("create room", () =>
{
room = new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
};
});
AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room and immediately exit", () =>
{
multiplayerScreen.ChildrenOfType<LoungeSubScreen>().Single().Open(room);
Schedule(() => Stack.CurrentScreen.Exit());
});
}
[Test] [Test]
public void TestJoinRoomWithoutPassword() public void TestJoinRoomWithoutPassword()
{ {

View File

@ -80,6 +80,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("room join password correct", () => lastJoinedPassword == "password"); AddAssert("room join password correct", () => lastJoinedPassword == "password");
} }
[Test]
public void TestJoinRoomWithPasswordViaKeyboardOnly()
{
DrawableRoom.PasswordEntryPopover passwordEntryPopover = null;
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
AddAssert("room join password correct", () => lastJoinedPassword == "password");
}
private void onRoomJoined(Room room, string password) private void onRoomJoined(Room room, string password)
{ {
lastJoinedRoom = room; lastJoinedRoom = room;

View File

@ -330,15 +330,15 @@ namespace osu.Game.Tests.Visual.Online
InputManager.ReleaseKey(Key.AltLeft); InputManager.ReleaseKey(Key.AltLeft);
} }
private void pressCloseDocumentKeys() => pressKeysFor(PlatformActionType.DocumentClose); private void pressCloseDocumentKeys() => pressKeysFor(PlatformAction.DocumentClose);
private void pressNewTabKeys() => pressKeysFor(PlatformActionType.TabNew); private void pressNewTabKeys() => pressKeysFor(PlatformAction.TabNew);
private void pressRestoreTabKeys() => pressKeysFor(PlatformActionType.TabRestore); private void pressRestoreTabKeys() => pressKeysFor(PlatformAction.TabRestore);
private void pressKeysFor(PlatformActionType type) private void pressKeysFor(PlatformAction type)
{ {
var binding = host.PlatformKeyBindings.First(b => ((PlatformAction)b.Action).ActionType == type); var binding = host.PlatformKeyBindings.First(b => (PlatformAction)b.Action == type);
foreach (var k in binding.KeyCombination.Keys) foreach (var k in binding.KeyCombination.Keys)
InputManager.PressKey((Key)k); InputManager.PressKey((Key)k);

View File

@ -29,6 +29,12 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo))); AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo)));
} }
[Test]
public void TestExcessMods()
{
AddStep("show excess mods score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo, true)));
}
private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score)
{ {
Child = new ContractedPanelMiddleContentContainer(workingBeatmap, score); Child = new ContractedPanelMiddleContentContainer(workingBeatmap, score);

View File

@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -36,6 +37,17 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
Beatmap = createTestBeatmap(author) Beatmap = createTestBeatmap(author)
})); }));
}
[Test]
public void TestExcessMods()
{
var author = new User { Username = "mapper_name" };
AddStep("show excess mods score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo, true)
{
Beatmap = createTestBeatmap(author)
}));
AddAssert("mapper name present", () => this.ChildrenOfType<OsuSpriteText>().Any(spriteText => spriteText.Current.Value == "mapper_name")); AddAssert("mapper name present", () => this.ChildrenOfType<OsuSpriteText>().Any(spriteText => spriteText.Current.Value == "mapper_name"));
} }
@ -50,9 +62,33 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("mapped by text not present", () => AddAssert("mapped by text not present", () =>
this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by"))); this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by")));
AddAssert("play time displayed", () => this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any());
} }
private void showPanel(ScoreInfo score) => Child = new ExpandedPanelMiddleContentContainer(score); [Test]
public void TestWithDefaultDate()
{
AddStep("show autoplay score", () =>
{
var ruleset = new OsuRuleset();
var mods = new Mod[] { ruleset.GetAutoplayMod() };
var beatmap = createTestBeatmap(null);
showPanel(new TestScoreInfo(ruleset.RulesetInfo)
{
Mods = mods,
Beatmap = beatmap,
Date = default,
});
});
AddAssert("play time not displayed", () => !this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any());
}
private void showPanel(ScoreInfo score) =>
Child = new ExpandedPanelMiddleContentContainer(score);
private BeatmapInfo createTestBeatmap(User author) private BeatmapInfo createTestBeatmap(User author)
{ {

View File

@ -8,7 +8,7 @@ using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.KeyBinding; using osu.Game.Overlays.Settings.Sections.Input;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings

View File

@ -11,10 +11,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneModDisplay : OsuTestScene public class TestSceneModDisplay : OsuTestScene
{ {
[TestCase(ExpansionMode.ExpandOnHover)] [Test]
[TestCase(ExpansionMode.AlwaysExpanded)] public void TestMode([Values] ExpansionMode mode)
[TestCase(ExpansionMode.AlwaysContracted)]
public void TestMode(ExpansionMode mode)
{ {
AddStep("create mod display", () => AddStep("create mod display", () =>
{ {

View File

@ -0,0 +1,49 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModFlowDisplay : OsuTestScene
{
private ModFlowDisplay modFlow;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = modFlow = new ModFlowDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.None,
Width = 200,
Current =
{
Value = new OsuRuleset().GetAllMods().ToArray(),
}
};
});
[Test]
public void TestWrapping()
{
AddSliderStep("icon size", 0.1f, 2, 1, val =>
{
if (modFlow != null)
modFlow.IconScale = val;
});
AddSliderStep("flow width", 100, 500, 200, val =>
{
if (modFlow != null)
modFlow.Width = val;
});
}
}
}

View File

@ -416,7 +416,6 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Position = new Vector2(-5, 25), Position = new Vector2(-5, 25),
Current = { BindTarget = modSelect.SelectedMods } Current = { BindTarget = modSelect.SelectedMods }
} }

View File

@ -1,8 +1,13 @@
// 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.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
[LocalisableEnum(typeof(BeatmapSetOnlineStatusEnumLocalisationMapper))]
public enum BeatmapSetOnlineStatus public enum BeatmapSetOnlineStatus
{ {
None = -3, None = -3,
@ -20,4 +25,40 @@ namespace osu.Game.Beatmaps
public static bool GrantsPerformancePoints(this BeatmapSetOnlineStatus status) public static bool GrantsPerformancePoints(this BeatmapSetOnlineStatus status)
=> status == BeatmapSetOnlineStatus.Ranked || status == BeatmapSetOnlineStatus.Approved; => status == BeatmapSetOnlineStatus.Ranked || status == BeatmapSetOnlineStatus.Approved;
} }
public class BeatmapSetOnlineStatusEnumLocalisationMapper : EnumLocalisationMapper<BeatmapSetOnlineStatus>
{
public override LocalisableString Map(BeatmapSetOnlineStatus value)
{
switch (value)
{
case BeatmapSetOnlineStatus.None:
return string.Empty;
case BeatmapSetOnlineStatus.Graveyard:
return BeatmapsetsStrings.ShowStatusGraveyard;
case BeatmapSetOnlineStatus.WIP:
return BeatmapsetsStrings.ShowStatusWip;
case BeatmapSetOnlineStatus.Pending:
return BeatmapsetsStrings.ShowStatusPending;
case BeatmapSetOnlineStatus.Ranked:
return BeatmapsetsStrings.ShowStatusRanked;
case BeatmapSetOnlineStatus.Approved:
return BeatmapsetsStrings.ShowStatusApproved;
case BeatmapSetOnlineStatus.Qualified:
return BeatmapsetsStrings.ShowStatusQualified;
case BeatmapSetOnlineStatus.Loved:
return BeatmapsetsStrings.ShowStatusLoved;
default:
throw new ArgumentOutOfRangeException(nameof(value), value, null);
}
}
}
} }

View File

@ -1,6 +1,8 @@
// 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.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
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.Shapes;
@ -28,7 +30,7 @@ namespace osu.Game.Beatmaps.Drawables
status = value; status = value;
Alpha = value == BeatmapSetOnlineStatus.None ? 0 : 1; Alpha = value == BeatmapSetOnlineStatus.None ? 0 : 1;
statusText.Text = value.ToString().ToUpperInvariant(); statusText.Text = value.GetLocalisableDescription().ToUpper();
} }
} }

View File

@ -10,6 +10,9 @@ namespace osu.Game.Graphics.UserInterface
[Description("default")] [Description("default")]
Default, Default,
[Description("soft")]
Soft,
[Description("button")] [Description("button")]
Button, Button,

View File

@ -32,20 +32,15 @@ namespace osu.Game.Graphics.UserInterface
public override bool OnPressed(PlatformAction action) public override bool OnPressed(PlatformAction action)
{ {
switch (action.ActionType) switch (action)
{ {
case PlatformActionType.LineEnd: case PlatformAction.MoveBackwardLine:
case PlatformActionType.LineStart: case PlatformAction.MoveForwardLine:
return false;
// Shift+delete is handled via PlatformAction on macOS. this is not so useful in the context of a SearchTextBox // Shift+delete is handled via PlatformAction on macOS. this is not so useful in the context of a SearchTextBox
// as we do not allow arrow key navigation in the first place (ie. the caret should always be at the end of text) // as we do not allow arrow key navigation in the first place (ie. the caret should always be at the end of text)
// Avoid handling it here to allow other components to potentially consume the shortcut. // Avoid handling it here to allow other components to potentially consume the shortcut.
case PlatformActionType.CharNext: case PlatformAction.DeleteForwardChar:
if (action.ActionMethod == PlatformActionMethod.Delete) return false;
return false;
break;
} }
return base.OnPressed(action); return base.OnPressed(action);

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -25,9 +26,13 @@ namespace osu.Game.Graphics.UserInterfaceV2
Flow.AutoSizeAxes = Axes.X; Flow.AutoSizeAxes = Axes.X;
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
AddInternal(new Background AddRangeInternal(new Drawable[]
{ {
Depth = 1 new Background
{
Depth = 1
},
new HoverClickSounds(HoverSampleSet.Soft)
}); });
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -50,9 +51,13 @@ namespace osu.Game.Graphics.UserInterfaceV2
Flow.AutoSizeAxes = Axes.X; Flow.AutoSizeAxes = Axes.X;
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
AddInternal(new OsuDirectorySelectorDirectory.Background AddRangeInternal(new Drawable[]
{ {
Depth = 1 new OsuDirectorySelectorDirectory.Background
{
Depth = 1
},
new HoverClickSounds(HoverSampleSet.Soft)
}); });
} }

View File

@ -16,6 +16,11 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
protected virtual string FileExtension { get; } = @".tmp"; protected virtual string FileExtension { get; } = @".tmp";
protected APIDownloadRequest()
{
base.Success += () => Success?.Invoke(filename);
}
protected override WebRequest CreateWebRequest() protected override WebRequest CreateWebRequest()
{ {
var file = Path.GetTempFileName(); var file = Path.GetTempFileName();
@ -39,12 +44,6 @@ namespace osu.Game.Online.API
TriggerSuccess(); TriggerSuccess();
} }
internal override void TriggerSuccess()
{
base.TriggerSuccess();
Success?.Invoke(filename);
}
public event APIProgressHandler Progressed; public event APIProgressHandler Progressed;
public new event APISuccessHandler<string> Success; public new event APISuccessHandler<string> Success;

View File

@ -0,0 +1,15 @@
// 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;
namespace osu.Game.Online.API
{
public class APIException : InvalidOperationException
{
public APIException(string messsage, Exception innerException)
: base(messsage, innerException)
{
}
}
}

View File

@ -25,6 +25,11 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
public new event APISuccessHandler<T> Success; public new event APISuccessHandler<T> Success;
protected APIRequest()
{
base.Success += () => Success?.Invoke(Result);
}
protected override void PostProcess() protected override void PostProcess()
{ {
base.PostProcess(); base.PostProcess();
@ -40,12 +45,6 @@ namespace osu.Game.Online.API
TriggerSuccess(); TriggerSuccess();
} }
internal override void TriggerSuccess()
{
base.TriggerSuccess();
Success?.Invoke(Result);
}
} }
/// <summary> /// <summary>
@ -79,7 +78,13 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
public event APIFailureHandler Failure; public event APIFailureHandler Failure;
private bool cancelled; private readonly object completionStateLock = new object();
/// <summary>
/// The state of this request, from an outside perspective.
/// This is used to ensure correct notification events are fired.
/// </summary>
private APIRequestCompletionState completionState;
private Action pendingFailure; private Action pendingFailure;
@ -116,12 +121,7 @@ namespace osu.Game.Online.API
PostProcess(); PostProcess();
API.Schedule(delegate API.Schedule(TriggerSuccess);
{
if (cancelled) return;
TriggerSuccess();
});
} }
/// <summary> /// <summary>
@ -131,16 +131,29 @@ namespace osu.Game.Online.API
{ {
} }
private bool succeeded; internal void TriggerSuccess()
internal virtual void TriggerSuccess()
{ {
succeeded = true; lock (completionStateLock)
{
if (completionState != APIRequestCompletionState.Waiting)
return;
completionState = APIRequestCompletionState.Completed;
}
Success?.Invoke(); Success?.Invoke();
} }
internal void TriggerFailure(Exception e) internal void TriggerFailure(Exception e)
{ {
lock (completionStateLock)
{
if (completionState != APIRequestCompletionState.Waiting)
return;
completionState = APIRequestCompletionState.Failed;
}
Failure?.Invoke(e); Failure?.Invoke(e);
} }
@ -148,10 +161,14 @@ namespace osu.Game.Online.API
public void Fail(Exception e) public void Fail(Exception e)
{ {
if (succeeded || cancelled) lock (completionStateLock)
return; {
// while it doesn't matter if code following this check is run more than once,
// this avoids unnecessarily performing work where we are already sure the user has been informed.
if (completionState != APIRequestCompletionState.Waiting)
return;
}
cancelled = true;
WebRequest?.Abort(); WebRequest?.Abort();
string responseString = WebRequest?.GetResponseString(); string responseString = WebRequest?.GetResponseString();
@ -181,7 +198,11 @@ namespace osu.Game.Online.API
/// <returns>Whether we are in a failed or cancelled state.</returns> /// <returns>Whether we are in a failed or cancelled state.</returns>
private bool checkAndScheduleFailure() private bool checkAndScheduleFailure()
{ {
if (pendingFailure == null) return cancelled; lock (completionStateLock)
{
if (pendingFailure == null)
return completionState == APIRequestCompletionState.Failed;
}
if (API == null) if (API == null)
pendingFailure(); pendingFailure();
@ -199,14 +220,6 @@ namespace osu.Game.Online.API
} }
} }
public class APIException : InvalidOperationException
{
public APIException(string messsage, Exception innerException)
: base(messsage, innerException)
{
}
}
public delegate void APIFailureHandler(Exception e); public delegate void APIFailureHandler(Exception e);
public delegate void APISuccessHandler(); public delegate void APISuccessHandler();

View File

@ -0,0 +1,23 @@
// 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.
namespace osu.Game.Online.API
{
public enum APIRequestCompletionState
{
/// <summary>
/// Not yet run or currently waiting on response.
/// </summary>
Waiting,
/// <summary>
/// Ran to completion.
/// </summary>
Completed,
/// <summary>
/// Cancelled or failed due to error.
/// </summary>
Failed
}
}

View File

@ -26,9 +26,9 @@ namespace osu.Game.Online.API.Requests
public enum BeatmapSetType public enum BeatmapSetType
{ {
Favourite, Favourite,
RankedAndApproved, Ranked,
Loved, Loved,
Unranked, Pending,
Graveyard Graveyard
} }
} }

View File

@ -70,7 +70,7 @@ namespace osu.Game.Online
return true; return true;
} }
// not ennough time has passed since the last poll. we do want to schedule a poll to happen, though. // not enough time has passed since the last poll. we do want to schedule a poll to happen, though.
scheduleNextPoll(); scheduleNextPoll();
return false; return false;
} }

View File

@ -932,7 +932,7 @@ namespace osu.Game
try try
{ {
Logger.Log($"Loading {component}...", level: LogLevel.Debug); Logger.Log($"Loading {component}...");
// Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called // Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called
// throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true // throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true
@ -952,7 +952,7 @@ namespace osu.Game
await task.ConfigureAwait(false); await task.ConfigureAwait(false);
Logger.Log($"Loaded {component}!", level: LogLevel.Debug); Logger.Log($"Loaded {component}!");
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {

View File

@ -208,7 +208,8 @@ namespace osu.Game.Overlays.BeatmapListing
{ {
var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList(); var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList();
if (sets.Count == 0) // If the previous request returned a null cursor, the API is indicating we can't paginate further (maybe there are no more beatmaps left).
if (sets.Count == 0 || response.Cursor == null)
noMoreResults = true; noMoreResults = true;
if (CurrentPage == 0) if (CurrentPage == 0)

View File

@ -2,11 +2,13 @@
// 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.Extensions.LocalisationExtensions;
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.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapSet namespace osu.Game.Overlays.BeatmapSet
{ {
@ -34,7 +36,7 @@ namespace osu.Game.Overlays.BeatmapSet
new OsuSpriteText new OsuSpriteText
{ {
Margin = new MarginPadding { Horizontal = 10f, Vertical = 2f }, Margin = new MarginPadding { Horizontal = 10f, Vertical = 2f },
Text = "EXPLICIT", Text = BeatmapsetsStrings.NsfwBadgeLabel.ToUpper(),
Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold),
Colour = OverlayColourProvider.Orange.Colour2, Colour = OverlayColourProvider.Orange.Colour2,
} }

View File

@ -374,17 +374,17 @@ namespace osu.Game.Overlays
public bool OnPressed(PlatformAction action) public bool OnPressed(PlatformAction action)
{ {
switch (action.ActionType) switch (action)
{ {
case PlatformActionType.TabNew: case PlatformAction.TabNew:
ChannelTabControl.SelectChannelSelectorTab(); ChannelTabControl.SelectChannelSelectorTab();
return true; return true;
case PlatformActionType.TabRestore: case PlatformAction.TabRestore:
channelManager.JoinLastClosedChannel(); channelManager.JoinLastClosedChannel();
return true; return true;
case PlatformActionType.DocumentClose: case PlatformAction.DocumentClose:
channelManager.LeaveChannel(channelManager.CurrentChannel.Value); channelManager.LeaveChannel(channelManager.CurrentChannel.Value);
return true; return true;
} }

View File

@ -46,11 +46,11 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
case BeatmapSetType.Loved: case BeatmapSetType.Loved:
return user.LovedBeatmapsetCount; return user.LovedBeatmapsetCount;
case BeatmapSetType.RankedAndApproved: case BeatmapSetType.Ranked:
return user.RankedAndApprovedBeatmapsetCount; return user.RankedBeatmapsetCount;
case BeatmapSetType.Unranked: case BeatmapSetType.Pending:
return user.UnrankedBeatmapsetCount; return user.PendingBeatmapsetCount;
default: default:
return 0; return 0;

View File

@ -19,9 +19,9 @@ namespace osu.Game.Overlays.Profile.Sections
Children = new[] Children = new[]
{ {
new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, UsersStrings.ShowExtraBeatmapsFavouriteTitle), new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, UsersStrings.ShowExtraBeatmapsFavouriteTitle),
new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User, UsersStrings.ShowExtraBeatmapsRankedTitle), new PaginatedBeatmapContainer(BeatmapSetType.Ranked, User, UsersStrings.ShowExtraBeatmapsRankedTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, UsersStrings.ShowExtraBeatmapsLovedTitle), new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, UsersStrings.ShowExtraBeatmapsLovedTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, UsersStrings.ShowExtraBeatmapsPendingTitle), new PaginatedBeatmapContainer(BeatmapSetType.Pending, User, UsersStrings.ShowExtraBeatmapsPendingTitle),
new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, UsersStrings.ShowExtraBeatmapsGraveyardTitle) new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, UsersStrings.ShowExtraBeatmapsGraveyardTitle)
}; };
} }

View File

@ -5,9 +5,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings;
namespace osu.Game.Overlays.KeyBinding namespace osu.Game.Overlays.Settings.Sections.Input
{ {
public class GlobalKeyBindingsSection : SettingsSection public class GlobalKeyBindingsSection : SettingsSection
{ {

View File

@ -4,11 +4,9 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Overlays.KeyBinding;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
namespace osu.Game.Overlays namespace osu.Game.Overlays.Settings.Sections.Input
{ {
public class KeyBindingPanel : SettingsSubPanel public class KeyBindingPanel : SettingsSubPanel
{ {

View File

@ -24,7 +24,7 @@ using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Overlays.KeyBinding namespace osu.Game.Overlays.Settings.Sections.Input
{ {
public class KeyBindingRow : Container, IFilterable public class KeyBindingRow : Container, IFilterable
{ {

View File

@ -9,11 +9,10 @@ using osu.Framework.Graphics;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osuTK; using osuTK;
namespace osu.Game.Overlays.KeyBinding namespace osu.Game.Overlays.Settings.Sections.Input
{ {
public abstract class KeyBindingsSubsection : SettingsSubsection public abstract class KeyBindingsSubsection : SettingsSubsection
{ {

View File

@ -4,10 +4,9 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
namespace osu.Game.Overlays.KeyBinding namespace osu.Game.Overlays.Settings.Sections.Input
{ {
public class RulesetBindingsSection : SettingsSection public class RulesetBindingsSection : SettingsSection
{ {

View File

@ -4,7 +4,7 @@
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Rulesets; using osu.Game.Rulesets;
namespace osu.Game.Overlays.KeyBinding namespace osu.Game.Overlays.Settings.Sections.Input
{ {
public class VariantBindingsSubsection : KeyBindingsSubsection public class VariantBindingsSubsection : KeyBindingsSubsection
{ {

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections; using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.Settings.Sections.Input;
using osuTK.Graphics; using osuTK.Graphics;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;

View File

@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// <summary> /// <summary>
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point. /// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
/// </summary> /// </summary>
public sealed override double DifficultyValue() public override double DifficultyValue()
{ {
double difficulty = 0; double difficulty = 0;
double weight = 1; double weight = 1;

View File

@ -502,8 +502,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{ {
if (!(HitObject is IHasComboInformation combo)) return; if (!(HitObject is IHasComboInformation combo)) return;
var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>(); AccentColour.Value = combo.GetComboColour(CurrentSkin);
AccentColour.Value = combo.GetComboColour(comboColours);
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,47 @@
// 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.Linq;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
#nullable enable
namespace osu.Game.Rulesets.Objects
{
public static class SliderPathExtensions
{
/// <summary>
/// Reverse the direction of this path.
/// </summary>
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset)
{
var points = sliderPath.ControlPoints.ToArray();
positionalOffset = points.Last().Position.Value;
sliderPath.ControlPoints.Clear();
PathType? lastType = null;
for (var i = 0; i < points.Length; i++)
{
var p = points[i];
p.Position.Value -= positionalOffset;
// propagate types forwards to last null type
if (i == points.Length - 1)
p.Type.Value = lastType;
else if (p.Type.Value != null)
{
var newType = p.Type.Value;
p.Type.Value = lastType;
lastType = newType;
}
sliderPath.ControlPoints.Insert(0, p);
}
}
}
}

View File

@ -1,9 +1,8 @@
// 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 JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Objects.Types namespace osu.Game.Rulesets.Objects.Types
@ -40,11 +39,21 @@ namespace osu.Game.Rulesets.Objects.Types
bool LastInCombo { get; set; } bool LastInCombo { get; set; }
/// <summary> /// <summary>
/// Retrieves the colour of the combo described by this <see cref="IHasComboInformation"/> object from a set of possible combo colours. /// Retrieves the colour of the combo described by this <see cref="IHasComboInformation"/> object.
/// Defaults to using <see cref="ComboIndex"/> to decide the colour.
/// </summary> /// </summary>
/// <param name="comboColours">A list of possible combo colours provided by the beatmap or skin.</param> /// <param name="skin">The skin to retrieve the combo colour from, if wanted.</param>
/// <returns>The colour of the combo described by this <see cref="IHasComboInformation"/> object.</returns> Color4 GetComboColour(ISkin skin) => GetSkinComboColour(this, skin, ComboIndex);
Color4 GetComboColour([NotNull] IReadOnlyList<Color4> comboColours) => comboColours.Count > 0 ? comboColours[ComboIndex % comboColours.Count] : Color4.White;
/// <summary>
/// Retrieves the colour of the combo described by a given <see cref="IHasComboInformation"/> object from a given skin.
/// </summary>
/// <param name="combo">The combo information, should be <c>this</c>.</param>
/// <param name="skin">The skin to retrieve the combo colour from.</param>
/// <param name="comboIndex">The index to retrieve the combo colour with.</param>
/// <returns></returns>
protected static Color4 GetSkinComboColour(IHasComboInformation combo, ISkin skin, int comboIndex)
{
return skin.GetConfig<SkinComboColourLookup, Color4>(new SkinComboColourLookup(comboIndex, combo))?.Value ?? Color4.White;
}
} }
} }

View File

@ -110,9 +110,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
bool selectionPerformed = performMouseDownActions(e); bool selectionPerformed = performMouseDownActions(e);
// even if a selection didn't occur, a drag event may still move the selection. // even if a selection didn't occur, a drag event may still move the selection.
prepareSelectionMovement(); bool movementPossible = prepareSelectionMovement();
return selectionPerformed || e.Button == MouseButton.Left; return selectionPerformed || (e.Button == MouseButton.Left && movementPossible);
} }
protected SelectionBlueprint<T> ClickedBlueprint { get; private set; } protected SelectionBlueprint<T> ClickedBlueprint { get; private set; }
@ -230,9 +230,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
public bool OnPressed(PlatformAction action) public bool OnPressed(PlatformAction action)
{ {
switch (action.ActionType) switch (action)
{ {
case PlatformActionType.SelectAll: case PlatformAction.SelectAll:
SelectAll(); SelectAll();
return true; return true;
} }
@ -427,19 +427,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// Attempts to begin the movement of any selected blueprints. /// Attempts to begin the movement of any selected blueprints.
/// </summary> /// </summary>
private void prepareSelectionMovement() /// <returns>Whether a movement is possible.</returns>
private bool prepareSelectionMovement()
{ {
if (!SelectionHandler.SelectedBlueprints.Any()) if (!SelectionHandler.SelectedBlueprints.Any())
return; return false;
// Any selected blueprint that is hovered can begin the movement of the group, however only the first item (according to SortForMovement) is used for movement. // Any selected blueprint that is hovered can begin the movement of the group, however only the first item (according to SortForMovement) is used for movement.
// A special case is added for when a click selection occurred before the drag // A special case is added for when a click selection occurred before the drag
if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered)) if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
return; return false;
// Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray(); movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray();
return true;
} }
/// <summary> /// <summary>

View File

@ -64,7 +64,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private bool canScaleX; private bool canScaleX;
/// <summary> /// <summary>
/// Whether vertical scale support should be enabled. /// Whether horizontal scaling support should be enabled.
/// </summary> /// </summary>
public bool CanScaleX public bool CanScaleX
{ {
@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private bool canScaleY; private bool canScaleY;
/// <summary> /// <summary>
/// Whether horizontal scale support should be enabled. /// Whether vertical scaling support should be enabled.
/// </summary> /// </summary>
public bool CanScaleY public bool CanScaleY
{ {
@ -95,6 +95,40 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
private bool canFlipX;
/// <summary>
/// Whether horizontal flipping support should be enabled.
/// </summary>
public bool CanFlipX
{
get => canFlipX;
set
{
if (canFlipX == value) return;
canFlipX = value;
recreate();
}
}
private bool canFlipY;
/// <summary>
/// Whether vertical flipping support should be enabled.
/// </summary>
public bool CanFlipY
{
get => canFlipY;
set
{
if (canFlipY == value) return;
canFlipY = value;
recreate();
}
}
private string text; private string text;
public string Text public string Text
@ -142,10 +176,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
return CanReverse && runOperationFromHotkey(OnReverse); return CanReverse && runOperationFromHotkey(OnReverse);
case Key.H: case Key.H:
return CanScaleX && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Horizontal) ?? false); return CanFlipX && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Horizontal) ?? false);
case Key.J: case Key.J:
return CanScaleY && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Vertical) ?? false); return CanFlipY && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Vertical) ?? false);
} }
return base.OnKeyDown(e); return base.OnKeyDown(e);
@ -214,6 +248,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (CanScaleX) addXScaleComponents(); if (CanScaleX) addXScaleComponents();
if (CanScaleX && CanScaleY) addFullScaleComponents(); if (CanScaleX && CanScaleY) addFullScaleComponents();
if (CanScaleY) addYScaleComponents(); if (CanScaleY) addYScaleComponents();
if (CanFlipX) addXFlipComponents();
if (CanFlipY) addYFlipComponents();
if (CanRotate) addRotationComponents(); if (CanRotate) addRotationComponents();
if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke());
} }
@ -231,8 +267,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void addYScaleComponents() private void addYScaleComponents()
{ {
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical));
addScaleHandle(Anchor.TopCentre); addScaleHandle(Anchor.TopCentre);
addScaleHandle(Anchor.BottomCentre); addScaleHandle(Anchor.BottomCentre);
} }
@ -247,12 +281,20 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void addXScaleComponents() private void addXScaleComponents()
{ {
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal));
addScaleHandle(Anchor.CentreLeft); addScaleHandle(Anchor.CentreLeft);
addScaleHandle(Anchor.CentreRight); addScaleHandle(Anchor.CentreRight);
} }
private void addXFlipComponents()
{
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal));
}
private void addYFlipComponents()
{
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical));
}
private void addButton(IconUsage icon, string tooltip, Action action) private void addButton(IconUsage icon, string tooltip, Action action)
{ {
var button = new SelectionBoxButton(icon, tooltip) var button = new SelectionBoxButton(icon, tooltip)

View File

@ -139,9 +139,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
public bool OnPressed(PlatformAction action) public bool OnPressed(PlatformAction action)
{ {
switch (action.ActionMethod) switch (action)
{ {
case PlatformActionMethod.Delete: case PlatformAction.Delete:
DeleteSelected(); DeleteSelected();
return true; return true;
} }

View File

@ -13,11 +13,9 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@ -31,22 +29,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private Timeline timeline { get; set; } private Timeline timeline { get; set; }
[Resolved]
private OsuColour colours { get; set; }
private DragEvent lastDragEvent; private DragEvent lastDragEvent;
private Bindable<HitObject> placement; private Bindable<HitObject> placement;
private SelectionBlueprint<HitObject> placementBlueprint; private SelectionBlueprint<HitObject> placementBlueprint;
private SelectableAreaBackground backgroundBox; // We want children within the timeline to be interactable
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => timeline.ScreenSpaceDrawQuad.Contains(screenSpacePos);
// we only care about checking vertical validity.
// this allows selecting and dragging selections before time=0.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
float localY = ToLocalSpace(screenSpacePos).Y;
return DrawRectangle.Top <= localY && DrawRectangle.Bottom >= localY;
}
public TimelineBlueprintContainer(HitObjectComposer composer) public TimelineBlueprintContainer(HitObjectComposer composer)
: base(composer) : base(composer)
@ -61,7 +49,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
AddInternal(backgroundBox = new SelectableAreaBackground AddInternal(new SelectableAreaBackground
{ {
Colour = Color4.Black, Colour = Color4.Black,
Depth = float.MaxValue, Depth = float.MaxValue,
@ -100,18 +88,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
protected override bool OnHover(HoverEvent e)
{
backgroundBox.FadeColour(colours.BlueLighter, 120, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
backgroundBox.FadeColour(Color4.Black, 600, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
{ {
handleScrollViaDrag(e); handleScrollViaDrag(e);
@ -184,7 +160,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
return new TimelineHitObjectBlueprint(item) return new TimelineHitObjectBlueprint(item)
{ {
OnDragHandled = handleScrollViaDrag OnDragHandled = handleScrollViaDrag,
}; };
} }
@ -212,6 +188,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private class SelectableAreaBackground : CompositeDrawable private class SelectableAreaBackground : CompositeDrawable
{ {
[Resolved]
private OsuColour colours { get; set; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
float localY = ToLocalSpace(screenSpacePos).Y;
return DrawRectangle.Top <= localY && DrawRectangle.Bottom >= localY;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -235,114 +220,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
}); });
} }
}
internal class TimelineSelectionHandler : EditorSelectionHandler, IKeyBindingHandler<GlobalAction> protected override bool OnHover(HoverEvent e)
{
// for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true;
public bool OnPressed(GlobalAction action)
{ {
switch (action) this.FadeColour(colours.BlueLighter, 120, Easing.OutQuint);
{ return base.OnHover(e);
case GlobalAction.EditorNudgeLeft:
nudgeSelection(-1);
return true;
case GlobalAction.EditorNudgeRight:
nudgeSelection(1);
return true;
}
return false;
} }
public void OnReleased(GlobalAction action) protected override void OnHoverLost(HoverLostEvent e)
{ {
} this.FadeColour(Color4.Black, 600, Easing.OutQuint);
base.OnHoverLost(e);
/// <summary>
/// Nudge the current selection by the specified multiple of beat divisor lengths,
/// based on the timing at the first object in the selection.
/// </summary>
/// <param name="amount">The direction and count of beat divisor lengths to adjust.</param>
private void nudgeSelection(int amount)
{
var selected = EditorBeatmap.SelectedHitObjects;
if (selected.Count == 0)
return;
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(selected.First().StartTime);
double adjustment = timingPoint.BeatLength / EditorBeatmap.BeatDivisor * amount;
EditorBeatmap.PerformOnSelection(h =>
{
h.StartTime += adjustment;
EditorBeatmap.Update(h);
});
}
}
private class TimelineDragBox : DragBox
{
// the following values hold the start and end X positions of the drag box in the timeline's local space,
// but with zoom unapplied in order to be able to compensate for positional changes
// while the timeline is being zoomed in/out.
private float? selectionStart;
private float selectionEnd;
[Resolved]
private Timeline timeline { get; set; }
public TimelineDragBox(Action<RectangleF> performSelect)
: base(performSelect)
{
}
protected override Drawable CreateBox() => new Box
{
RelativeSizeAxes = Axes.Y,
Alpha = 0.3f
};
public override bool HandleDrag(MouseButtonEvent e)
{
selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom;
// only calculate end when a transition is not in progress to avoid bouncing.
if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom))
selectionEnd = e.MousePosition.X / timeline.CurrentZoom;
updateDragBoxPosition();
return true;
}
private void updateDragBoxPosition()
{
if (selectionStart == null)
return;
float rescaledStart = selectionStart.Value * timeline.CurrentZoom;
float rescaledEnd = selectionEnd * timeline.CurrentZoom;
Box.X = Math.Min(rescaledStart, rescaledEnd);
Box.Width = Math.Abs(rescaledStart - rescaledEnd);
var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat;
// we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment.
boxScreenRect.Y -= boxScreenRect.Height;
boxScreenRect.Height *= 2;
PerformSelection?.Invoke(boxScreenRect);
}
public override void Hide()
{
base.Hide();
selectionStart = null;
} }
} }

View File

@ -0,0 +1,79 @@
// 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.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineDragBox : DragBox
{
// the following values hold the start and end X positions of the drag box in the timeline's local space,
// but with zoom unapplied in order to be able to compensate for positional changes
// while the timeline is being zoomed in/out.
private float? selectionStart;
private float selectionEnd;
[Resolved]
private Timeline timeline { get; set; }
public TimelineDragBox(Action<RectangleF> performSelect)
: base(performSelect)
{
}
protected override Drawable CreateBox() => new Box
{
RelativeSizeAxes = Axes.Y,
Alpha = 0.3f
};
public override bool HandleDrag(MouseButtonEvent e)
{
// The dragbox should only be active if the mouseDownPosition.Y is within this drawable's bounds.
float localY = ToLocalSpace(e.ScreenSpaceMouseDownPosition).Y;
if (DrawRectangle.Top > localY || DrawRectangle.Bottom < localY)
return false;
selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom;
// only calculate end when a transition is not in progress to avoid bouncing.
if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom))
selectionEnd = e.MousePosition.X / timeline.CurrentZoom;
updateDragBoxPosition();
return true;
}
private void updateDragBoxPosition()
{
if (selectionStart == null)
return;
float rescaledStart = selectionStart.Value * timeline.CurrentZoom;
float rescaledEnd = selectionEnd * timeline.CurrentZoom;
Box.X = Math.Min(rescaledStart, rescaledEnd);
Box.Width = Math.Abs(rescaledStart - rescaledEnd);
var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat;
// we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment.
boxScreenRect.Y -= boxScreenRect.Height;
boxScreenRect.Height *= 2;
PerformSelection?.Invoke(boxScreenRect);
}
public override void Hide()
{
base.Hide();
selectionStart = null;
}
}
}

View File

@ -2,7 +2,6 @@
// 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 JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -153,11 +152,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
break; break;
case IHasComboInformation combo: case IHasComboInformation combo:
{ colour = combo.GetComboColour(skin);
var comboColours = skin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
colour = combo.GetComboColour(comboColours);
break; break;
}
default: default:
return; return;

View File

@ -0,0 +1,65 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
internal class TimelineSelectionHandler : EditorSelectionHandler, IKeyBindingHandler<GlobalAction>
{
[Resolved]
private Timeline timeline { get; set; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => timeline.ScreenSpaceDrawQuad.Contains(screenSpacePos);
// for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true;
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.EditorNudgeLeft:
nudgeSelection(-1);
return true;
case GlobalAction.EditorNudgeRight:
nudgeSelection(1);
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
/// <summary>
/// Nudge the current selection by the specified multiple of beat divisor lengths,
/// based on the timing at the first object in the selection.
/// </summary>
/// <param name="amount">The direction and count of beat divisor lengths to adjust.</param>
private void nudgeSelection(int amount)
{
var selected = EditorBeatmap.SelectedHitObjects;
if (selected.Count == 0)
return;
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(selected.First().StartTime);
double adjustment = timingPoint.BeatLength / EditorBeatmap.BeatDivisor * amount;
EditorBeatmap.PerformOnSelection(h =>
{
h.StartTime += adjustment;
EditorBeatmap.Update(h);
});
}
}
}

View File

@ -80,7 +80,7 @@ namespace osu.Game.Screens.Edit.Compose
public bool OnPressed(PlatformAction action) public bool OnPressed(PlatformAction action)
{ {
if (action.ActionType == PlatformActionType.Copy) if (action == PlatformAction.Copy)
host.GetClipboard().SetText(formatSelectionAsString()); host.GetClipboard().SetText(formatSelectionAsString());
return false; return false;

View File

@ -330,29 +330,29 @@ namespace osu.Game.Screens.Edit
public bool OnPressed(PlatformAction action) public bool OnPressed(PlatformAction action)
{ {
switch (action.ActionType) switch (action)
{ {
case PlatformActionType.Cut: case PlatformAction.Cut:
Cut(); Cut();
return true; return true;
case PlatformActionType.Copy: case PlatformAction.Copy:
Copy(); Copy();
return true; return true;
case PlatformActionType.Paste: case PlatformAction.Paste:
Paste(); Paste();
return true; return true;
case PlatformActionType.Undo: case PlatformAction.Undo:
Undo(); Undo();
return true; return true;
case PlatformActionType.Redo: case PlatformAction.Redo:
Redo(); Redo();
return true; return true;
case PlatformActionType.Save: case PlatformAction.Save:
Save(); Save();
return true; return true;
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK.Graphics; using osuTK.Graphics;
@ -64,6 +65,7 @@ namespace osu.Game.Screens.Edit
private EditorClock clock { get; set; } private EditorClock clock { get; set; }
public RowBackground(object item) public RowBackground(object item)
: base(HoverSampleSet.Soft)
{ {
Item = item; Item = item;

View File

@ -108,7 +108,11 @@ namespace osu.Game.Screens.OnlinePlay
difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) }; difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) };
beatmapText.Clear(); beatmapText.Clear();
beatmapText.AddLink(Item.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString()); beatmapText.AddLink(Item.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text =>
{
text.Truncate = true;
text.RelativeSizeAxes = Axes.X;
});
authorText.Clear(); authorText.Clear();
@ -147,84 +151,96 @@ namespace osu.Game.Screens.OnlinePlay
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Beatmap = { BindTarget = beatmap } Beatmap = { BindTarget = beatmap }
}, },
new FillFlowContainer new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 8 }, ColumnDimensions = new[]
Spacing = new Vector2(8, 0),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{ {
difficultyIconContainer = new Container new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{ {
Anchor = Anchor.CentreLeft, difficultyIconContainer = new Container
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
beatmapText = new LinkFlowContainer(fontParameters) { AutoSizeAxes = Axes.Both }, Anchor = Anchor.CentreLeft,
new FillFlowContainer Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Left = 8, Right = 8, },
},
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
AutoSizeAxes = Axes.Both, beatmapText = new LinkFlowContainer(fontParameters)
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0),
Children = new Drawable[]
{ {
new FillFlowContainer AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0),
Children = new Drawable[]
{ {
AutoSizeAxes = Axes.Both, new FillFlowContainer
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0),
Children = new Drawable[]
{ {
authorText = new LinkFlowContainer(fontParameters) { AutoSizeAxes = Axes.Both }, AutoSizeAxes = Axes.Both,
explicitContentPill = new ExplicitContentBeatmapPill Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0),
Children = new Drawable[]
{ {
Alpha = 0f, authorText = new LinkFlowContainer(fontParameters) { AutoSizeAxes = Axes.Both },
Anchor = Anchor.CentreLeft, explicitContentPill = new ExplicitContentBeatmapPill
Origin = Anchor.CentreLeft, {
Margin = new MarginPadding { Top = 3f }, Alpha = 0f,
} Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Top = 3f },
}
},
}, },
}, new Container
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Child = modDisplay = new ModDisplay
{ {
Scale = new Vector2(0.4f), Anchor = Anchor.CentreLeft,
ExpansionMode = ExpansionMode.AlwaysExpanded Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Child = modDisplay = new ModDisplay
{
Scale = new Vector2(0.4f),
ExpansionMode = ExpansionMode.AlwaysExpanded
}
} }
} }
} }
} }
},
new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Left = 8, Right = 10, },
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
ChildrenEnumerable = CreateButtons().Select(button => button.With(b =>
{
b.Anchor = Anchor.Centre;
b.Origin = Anchor.Centre;
}))
} }
} }
} }
}, },
new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5),
X = -10,
ChildrenEnumerable = CreateButtons().Select(button => button.With(b =>
{
b.Anchor = Anchor.Centre;
b.Origin = Anchor.Centre;
}))
}
} }
}; };
} }

View File

@ -390,6 +390,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
base.LoadComplete(); base.LoadComplete();
Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextbox)); Schedule(() => GetContainingInputManager().ChangeFocus(passwordTextbox));
passwordTextbox.OnCommit += (_, __) => JoinRequested?.Invoke(room, passwordTextbox.Text);
} }
} }
} }

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 System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -30,17 +31,19 @@ namespace osu.Game.Screens.OnlinePlay
[Cached] [Cached]
public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack
{ {
public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; public override bool CursorVisible => (screenStack?.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true;
// this is required due to PlayerLoader eventually being pushed to the main stack // this is required due to PlayerLoader eventually being pushed to the main stack
// while leases may be taken out by a subscreen. // while leases may be taken out by a subscreen.
public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool DisallowExternalBeatmapRulesetChanges => true;
private readonly MultiplayerWaveContainer waves; private MultiplayerWaveContainer waves;
private readonly OsuButton createButton; private OsuButton createButton;
private readonly LoungeSubScreen loungeSubScreen;
private readonly ScreenStack screenStack; private ScreenStack screenStack;
private LoungeSubScreen loungeSubScreen;
private readonly IBindable<bool> isIdle = new BindableBool(); private readonly IBindable<bool> isIdle = new BindableBool();
@ -54,7 +57,7 @@ namespace osu.Game.Screens.OnlinePlay
private readonly Bindable<FilterCriteria> currentFilter = new Bindable<FilterCriteria>(new FilterCriteria()); private readonly Bindable<FilterCriteria> currentFilter = new Bindable<FilterCriteria>(new FilterCriteria());
[Cached] [Cached]
private OngoingOperationTracker ongoingOperationTracker { get; set; } private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private MusicController music { get; set; } private MusicController music { get; set; }
@ -65,11 +68,14 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved] [Resolved]
protected IAPIProvider API { get; private set; } protected IAPIProvider API { get; private set; }
[Resolved(CanBeNull = true)]
private IdleTracker idleTracker { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private OsuLogo logo { get; set; } private OsuLogo logo { get; set; }
private readonly Drawable header; private Drawable header;
private readonly Drawable headerBackground; private Drawable headerBackground;
protected OnlinePlayScreen() protected OnlinePlayScreen()
{ {
@ -78,6 +84,14 @@ namespace osu.Game.Screens.OnlinePlay
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING };
RoomManager = CreateRoomManager();
}
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
[BackgroundDependencyLoader]
private void load()
{
var backgroundColour = Color4Extensions.FromHex(@"3e3a44"); var backgroundColour = Color4Extensions.FromHex(@"3e3a44");
InternalChild = waves = new MultiplayerWaveContainer InternalChild = waves = new MultiplayerWaveContainer
@ -144,27 +158,14 @@ namespace osu.Game.Screens.OnlinePlay
}; };
button.Action = () => OpenNewRoom(); button.Action = () => OpenNewRoom();
}), }),
RoomManager = CreateRoomManager(), RoomManager,
ongoingOperationTracker = new OngoingOperationTracker() ongoingOperationTracker,
} }
}; };
screenStack.ScreenPushed += screenPushed; // a lot of the functionality in this class depends on loungeSubScreen being in a ready to go state.
screenStack.ScreenExited += screenExited; // as such, we intentionally load this inline so it is ready alongside this screen.
LoadComponent(loungeSubScreen = CreateLounge());
screenStack.Push(loungeSubScreen = CreateLounge());
}
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
[BackgroundDependencyLoader(true)]
private void load(IdleTracker idleTracker)
{
apiState.BindTo(API.State);
apiState.BindValueChanged(onlineStateChanged, true);
if (idleTracker != null)
isIdle.BindTo(idleTracker.IsIdle);
} }
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() => private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
@ -179,7 +180,20 @@ namespace osu.Game.Screens.OnlinePlay
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true);
screenStack.ScreenPushed += screenPushed;
screenStack.ScreenExited += screenExited;
screenStack.Push(loungeSubScreen);
apiState.BindTo(API.State);
apiState.BindValueChanged(onlineStateChanged, true);
if (idleTracker != null)
{
isIdle.BindTo(idleTracker.IsIdle);
isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true);
}
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -222,7 +236,9 @@ namespace osu.Game.Screens.OnlinePlay
this.FadeIn(250); this.FadeIn(250);
this.ScaleTo(1, 250, Easing.OutSine); this.ScaleTo(1, 250, Easing.OutSine);
screenStack.CurrentScreen?.OnResuming(last); Debug.Assert(screenStack.CurrentScreen != null);
screenStack.CurrentScreen.OnResuming(last);
base.OnResuming(last); base.OnResuming(last);
UpdatePollingRate(isIdle.Value); UpdatePollingRate(isIdle.Value);
@ -233,14 +249,16 @@ namespace osu.Game.Screens.OnlinePlay
this.ScaleTo(1.1f, 250, Easing.InSine); this.ScaleTo(1.1f, 250, Easing.InSine);
this.FadeOut(250); this.FadeOut(250);
screenStack.CurrentScreen?.OnSuspending(next); Debug.Assert(screenStack.CurrentScreen != null);
screenStack.CurrentScreen.OnSuspending(next);
UpdatePollingRate(isIdle.Value); UpdatePollingRate(isIdle.Value);
} }
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
if (screenStack.CurrentScreen?.OnExiting(next) == true) var subScreen = screenStack.CurrentScreen as Drawable;
if (subScreen?.IsLoaded == true && screenStack.CurrentScreen.OnExiting(next))
return true; return true;
RoomManager.PartRoom(); RoomManager.PartRoom();

View File

@ -172,7 +172,6 @@ namespace osu.Game.Screens.Play
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 20 }, Margin = new MarginPadding { Top = 20 },
Current = mods Current = mods
}, },

View File

@ -15,24 +15,26 @@ using osuTK;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class ModDisplay : Container, IHasCurrentValue<IReadOnlyList<Mod>> /// <summary>
/// Displays a single-line horizontal auto-sized flow of mods. For cases where wrapping is required, use <see cref="ModFlowDisplay"/> instead.
/// </summary>
public class ModDisplay : CompositeDrawable, IHasCurrentValue<IReadOnlyList<Mod>>
{ {
private const int fade_duration = 1000; private const int fade_duration = 1000;
public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover;
private readonly Bindable<IReadOnlyList<Mod>> current = new Bindable<IReadOnlyList<Mod>>(); private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>();
public Bindable<IReadOnlyList<Mod>> Current public Bindable<IReadOnlyList<Mod>> Current
{ {
get => current; get => current.Current;
set set
{ {
if (value == null) if (value == null)
throw new ArgumentNullException(nameof(value)); throw new ArgumentNullException(nameof(value));
current.UnbindBindings(); current.Current = value;
current.BindTo(value);
} }
} }
@ -42,51 +44,34 @@ namespace osu.Game.Screens.Play.HUD
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Child = new FillFlowContainer InternalChild = iconsContainer = new ReverseChildIDFillFlowContainer<ModIcon>
{ {
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
iconsContainer = new ReverseChildIDFillFlowContainer<ModIcon>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
},
},
}; };
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Current.UnbindAll();
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
Current.BindValueChanged(mods => Current.BindValueChanged(updateDisplay, true);
{
iconsContainer.Clear();
if (mods.NewValue != null)
{
foreach (Mod mod in mods.NewValue)
iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) });
appearTransform();
}
}, true);
iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint); iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint);
} }
private void updateDisplay(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
iconsContainer.Clear();
if (mods.NewValue == null) return;
foreach (Mod mod in mods.NewValue)
iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) });
appearTransform();
}
private void appearTransform() private void appearTransform()
{ {
expand(); expand();

View File

@ -0,0 +1,83 @@
// 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.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// A horizontally wrapping display of mods. For cases where wrapping is not required, use <see cref="ModDisplay"/> instead.
/// </summary>
public class ModFlowDisplay : ReverseChildIDFillFlowContainer<ModIcon>, IHasCurrentValue<IReadOnlyList<Mod>>
{
private const int fade_duration = 1000;
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>();
public Bindable<IReadOnlyList<Mod>> Current
{
get => current.Current;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
current.Current = value;
}
}
private float iconScale = 1;
public float IconScale
{
get => iconScale;
set
{
iconScale = value;
updateDisplay();
}
}
public ModFlowDisplay()
{
Direction = FillDirection.Full;
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => updateDisplay(), true);
this.FadeInFromZero(fade_duration, Easing.OutQuint);
}
private void updateDisplay()
{
Clear();
if (current.Value == null) return;
Spacing = new Vector2(0, -12 * iconScale);
foreach (Mod mod in current.Value)
{
Add(new ModIcon(mod)
{
Scale = new Vector2(0.6f * iconScale),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
});
}
}
}
}

View File

@ -282,7 +282,6 @@ namespace osu.Game.Screens.Play
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
}; };
protected PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay(); protected PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();

View File

@ -131,14 +131,14 @@ namespace osu.Game.Screens.Ranking.Contracted
createStatistic("Accuracy", $"{score.Accuracy.FormatAccuracy()}"), createStatistic("Accuracy", $"{score.Accuracy.FormatAccuracy()}"),
} }
}, },
new ModDisplay new ModFlowDisplay
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Y,
ExpansionMode = ExpansionMode.AlwaysExpanded, RelativeSizeAxes = Axes.X,
Current = { Value = score.Mods }, Current = { Value = score.Mods },
Scale = new Vector2(0.5f), IconScale = 0.5f,
} }
} }
} }

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 System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -79,162 +80,155 @@ namespace osu.Game.Screens.Ranking.Expanded
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).Result; var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).Result;
InternalChildren = new Drawable[] AddInternal(new FillFlowContainer
{ {
new FillFlowContainer RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, new FillFlowContainer
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{ {
new FillFlowContainer Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
Anchor = Anchor.TopCentre, new OsuSpriteText
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
new OsuSpriteText Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = 40 },
RelativeSizeAxes = Axes.X,
Height = 230,
Child = new AccuracyCircle(score, withFlair)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.Centre,
Origin = Anchor.TopCentre, Origin = Anchor.Centre,
Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), RelativeSizeAxes = Axes.Both,
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), FillMode = FillMode.Fit,
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, }
Truncate = true, },
}, scoreCounter = new TotalScoreCounter
new OsuSpriteText {
Margin = new MarginPadding { Top = 0, Bottom = 5 },
Current = { Value = 0 },
Alpha = 0,
AlwaysPresent = true
},
starAndModDisplay = new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{ {
Anchor = Anchor.TopCentre, new StarRatingDisplay(starDifficulty)
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = 40 },
RelativeSizeAxes = Axes.X,
Height = 230,
Child = new AccuracyCircle(score, withFlair)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre, Origin = Anchor.CentreLeft
RelativeSizeAxes = Axes.Both, },
FillMode = FillMode.Fit, }
} },
}, new FillFlowContainer
scoreCounter = new TotalScoreCounter {
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
Margin = new MarginPadding { Top = 0, Bottom = 5 }, new OsuSpriteText
Current = { Value = 0 },
Alpha = 0,
AlwaysPresent = true
},
starAndModDisplay = new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{ {
new StarRatingDisplay(starDifficulty) Anchor = Anchor.TopCentre,
{ Origin = Anchor.TopCentre,
Anchor = Anchor.CentreLeft, Text = beatmap.Version,
Origin = Anchor.CentreLeft Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
}, },
} new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))
},
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
new OsuSpriteText Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
}.With(t =>
{
if (!string.IsNullOrEmpty(creator))
{ {
Anchor = Anchor.TopCentre, t.AddText("mapped by ");
Origin = Anchor.TopCentre, t.AddText(creator, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
Text = beatmap.Version, }
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), })
}, }
new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) },
{ }
Anchor = Anchor.TopCentre, },
Origin = Anchor.TopCentre, new FillFlowContainer
AutoSizeAxes = Axes.Both, {
Direction = FillDirection.Horizontal, RelativeSizeAxes = Axes.X,
}.With(t => AutoSizeAxes = Axes.Y,
{ Direction = FillDirection.Vertical,
if (!string.IsNullOrEmpty(creator)) Spacing = new Vector2(0, 5),
{ Children = new Drawable[]
t.AddText("mapped by ");
t.AddText(creator, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
}
})
}
},
}
},
new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, new GridContainer
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Children = new Drawable[]
{ {
new GridContainer RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] { topStatistics.Cast<Drawable>().ToArray() },
RowDimensions = new[]
{ {
RelativeSizeAxes = Axes.X, new Dimension(GridSizeMode.AutoSize),
AutoSizeAxes = Axes.Y, }
Content = new[] { topStatistics.Cast<Drawable>().ToArray() }, },
RowDimensions = new[] new GridContainer
{ {
new Dimension(GridSizeMode.AutoSize), RelativeSizeAxes = Axes.X,
} AutoSizeAxes = Axes.Y,
}, Content = new[] { bottomStatistics.Where(s => s.Result <= HitResult.Perfect).ToArray() },
new GridContainer RowDimensions = new[]
{ {
RelativeSizeAxes = Axes.X, new Dimension(GridSizeMode.AutoSize),
AutoSizeAxes = Axes.Y, }
Content = new[] { bottomStatistics.Where(s => s.Result <= HitResult.Perfect).ToArray() }, },
RowDimensions = new[] new GridContainer
{ {
new Dimension(GridSizeMode.AutoSize), RelativeSizeAxes = Axes.X,
} AutoSizeAxes = Axes.Y,
}, Content = new[] { bottomStatistics.Where(s => s.Result > HitResult.Perfect).ToArray() },
new GridContainer RowDimensions = new[]
{ {
RelativeSizeAxes = Axes.X, new Dimension(GridSizeMode.AutoSize),
AutoSizeAxes = Axes.Y,
Content = new[] { bottomStatistics.Where(s => s.Result > HitResult.Perfect).ToArray() },
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
}
} }
} }
} }
} }
},
new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold),
Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}"
} }
}; });
if (score.Date != default)
AddInternal(new PlayedOnText(score.Date));
if (score.Mods.Any()) if (score.Mods.Any())
{ {
@ -276,5 +270,16 @@ namespace osu.Game.Screens.Ranking.Expanded
FinishTransforms(true); FinishTransforms(true);
}); });
} }
public class PlayedOnText : OsuSpriteText
{
public PlayedOnText(DateTimeOffset time)
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold);
Text = $"Played on {time.ToLocalTime():d MMMM yyyy HH:mm}";
}
}
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -136,10 +137,10 @@ namespace osu.Game.Skinning
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
// todo: this code is pulled from LegacySkin and should not exist.
// will likely change based on how databased storage of skin configuration goes.
switch (lookup) switch (lookup)
{ {
// todo: this code is pulled from LegacySkin and should not exist.
// will likely change based on how databased storage of skin configuration goes.
case GlobalSkinColours global: case GlobalSkinColours global:
switch (global) switch (global)
{ {
@ -148,9 +149,15 @@ namespace osu.Game.Skinning
} }
break; break;
case SkinComboColourLookup comboColour:
return SkinUtils.As<TValue>(new Bindable<Color4>(getComboColour(Configuration, comboColour.ColourIndex)));
} }
return null; return null;
} }
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
=> source.ComboColours[colourIndex % source.ComboColours.Count];
} }
} }

View File

@ -170,6 +170,8 @@ namespace osu.Game.Skinning.Editor
SelectionBox.CanRotate = true; SelectionBox.CanRotate = true;
SelectionBox.CanScaleX = true; SelectionBox.CanScaleX = true;
SelectionBox.CanScaleY = true; SelectionBox.CanScaleY = true;
SelectionBox.CanFlipX = true;
SelectionBox.CanFlipY = true;
SelectionBox.CanReverse = false; SelectionBox.CanReverse = false;
} }

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