1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 10:52:53 +08:00

Merge branch 'master' into gcc-abstraction

This commit is contained in:
smoogipoo 2021-04-16 20:14:53 +09:00
commit 5e82f40d67
133 changed files with 3871 additions and 1352 deletions

View File

@ -2,13 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Input.StateChanges;
using osu.Framework.Utils;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.EmptyFreeform.Replays
{
@ -21,26 +19,13 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
protected override bool IsImportant(EmptyFreeformReplayFrame frame) => frame.Actions.Any();
protected Vector2 Position
{
get
{
var frame = CurrentFrame;
if (frame == null)
return Vector2.Zero;
Debug.Assert(CurrentTime != null);
return Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time);
}
}
public override void CollectPendingInputs(List<IInput> inputs)
{
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
inputs.Add(new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(Position),
Position = GamefieldToScreenSpace(position),
});
inputs.Add(new ReplayState<EmptyFreeformAction>
{

View File

@ -2,12 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Input.StateChanges;
using osu.Framework.Utils;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Pippidon.Replays
{
@ -20,26 +18,13 @@ namespace osu.Game.Rulesets.Pippidon.Replays
protected override bool IsImportant(PippidonReplayFrame frame) => true;
protected Vector2 Position
{
get
{
var frame = CurrentFrame;
if (frame == null)
return Vector2.Zero;
Debug.Assert(CurrentTime != null);
return NextFrame != null ? Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time) : frame.Position;
}
}
public override void CollectPendingInputs(List<IInput> inputs)
{
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
inputs.Add(new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(Position)
Position = GamefieldToScreenSpace(position)
});
}
}

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.410.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.416.0" />
</ItemGroup>
</Project>

View File

@ -7,6 +7,8 @@ using Android.OS;
using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
using osu.Game.Utils;
using Xamarin.Essentials;
namespace osu.Android
{
@ -72,5 +74,14 @@ namespace osu.Android
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
private class AndroidBatteryInfo : BatteryInfo
{
public override double ChargeLevel => Battery.ChargeLevel;
public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery;
}
}
}

View File

@ -6,5 +6,6 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BATTERY_STATS" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
</manifest>

View File

@ -63,5 +63,8 @@
<Version>5.0.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>

View File

@ -125,10 +125,6 @@ namespace osu.Game.Rulesets.Catch.Replays
private void addFrame(double time, float? position = null, bool dashing = false)
{
// todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
if (Replay.Frames.Count == 0)
Replay.Frames.Add(new CatchReplayFrame(time - 1, position, false, null));
var last = currentFrame;
currentFrame = new CatchReplayFrame(time, position, dashing, last);
Replay.Frames.Add(currentFrame);

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Input.StateChanges;
using osu.Framework.Utils;
@ -20,29 +19,14 @@ namespace osu.Game.Rulesets.Catch.Replays
protected override bool IsImportant(CatchReplayFrame frame) => frame.Actions.Any();
protected float? Position
{
get
{
var frame = CurrentFrame;
if (frame == null)
return null;
Debug.Assert(CurrentTime != null);
return NextFrame != null ? Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time) : frame.Position;
}
}
public override void CollectPendingInputs(List<IInput> inputs)
{
if (!Position.HasValue) return;
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
inputs.Add(new CatchReplayState
{
PressedActions = CurrentFrame?.Actions ?? new List<CatchAction>(),
CatcherX = Position.Value
CatcherX = position
});
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Tests
/// <summary>
/// The number of frames which are generated at the start of a replay regardless of hitobject content.
/// </summary>
private const int frame_offset = 1;
private const int frame_offset = 0;
[Test]
public void TestSingleNote()
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time");
Assert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time");
@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.IsTrue(generated.Frames.Count == frame_offset + 3, "Replay must have 3 generated frames");
Assert.AreEqual(generated.Frames.Count, frame_offset + 3, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time");
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time");

View File

@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.UpdateTimeAndPosition(result);
if (PlacementActive)
if (PlacementActive == PlacementState.Active)
{
if (result.Time is double endTime)
{

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
base.UpdateTimeAndPosition(result);
if (!PlacementActive)
if (PlacementActive == PlacementState.Waiting)
Column = result.Playfield as Column;
}
}

View File

@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Mirror";
public override string Acronym => "MR";
public override ModType Type => ModType.Conversion;
public override string Description => "Notes are flipped horizontally.";
public override double ScoreMultiplier => 1;
public override bool Ranked => true;

View File

@ -70,10 +70,6 @@ namespace osu.Game.Rulesets.Mania.Replays
}
}
// todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame.
if (Replay.Frames.Count == 0)
Replay.Frames.Add(new ManiaReplayFrame(group.First().Time - 1));
Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray()));
}

View File

@ -0,0 +1,260 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
{
[TestFixture]
public class CheckOffscreenObjectsTest
{
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE * 0.5f;
private CheckOffscreenObjects check;
[SetUp]
public void Setup()
{
check = new CheckOffscreenObjects();
}
[Test]
public void TestCircleInCenter()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 3000,
Position = playfield_centre // Playfield is 640 x 480.
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestCircleNearEdge()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 3000,
Position = new Vector2(5, 5)
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestCircleNearEdgeStackedOffscreen()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 3000,
Position = new Vector2(5, 5),
StackHeight = 5
}
}
};
assertOffscreenCircle(beatmap);
}
[Test]
public void TestCircleOffscreen()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 3000,
Position = new Vector2(0, 0)
}
}
};
assertOffscreenCircle(beatmap);
}
[Test]
public void TestSliderInCenter()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = new Vector2(420, 240),
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(-100, 0))
}),
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestSliderNearEdge()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
}),
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestSliderNearEdgeStackedOffscreen()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
}),
StackHeight = 5
}
}
};
assertOffscreenSlider(beatmap);
}
[Test]
public void TestSliderOffscreenStart()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = new Vector2(0, 0),
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(playfield_centre)
}),
}
}
};
assertOffscreenSlider(beatmap);
}
[Test]
public void TestSliderOffscreenEnd()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(-playfield_centre)
}),
}
}
};
assertOffscreenSlider(beatmap);
}
[Test]
public void TestSliderOffscreenPath()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = playfield_centre,
Path = new SliderPath(new[]
{
// Circular arc shoots over the top of the screen.
new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(-100, -200)),
new PathControlPoint(new Vector2(100, -200))
}),
}
}
};
assertOffscreenSlider(beatmap);
}
private void assertOffscreenCircle(IBeatmap beatmap)
{
var issues = check.Run(beatmap).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
}
private void assertOffscreenSlider(IBeatmap beatmap)
{
var issues = check.Run(beatmap).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider);
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneOsuEditorSelectInvalidPath : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestSelectDoesNotModify()
{
Slider slider = new Slider { StartTime = 0, Position = new Vector2(320, 40) };
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(-100, 0)),
new PathControlPoint(new Vector2(100, 20))
};
int preSelectVersion = -1;
AddStep("add slider", () =>
{
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
preSelectVersion = slider.Path.Version.Value;
});
AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddAssert("slider same path", () => slider.Path.Version.Value == preSelectVersion);
}
}
}

View File

@ -0,0 +1,198 @@
// 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.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
public class TestSceneSliderLengthValidity : TestSceneOsuEditor
{
private OsuPlayfield playfield;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("get playfield", () => playfield = Editor.ChildrenOfType<OsuPlayfield>().First());
AddStep("seek to first timing point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time));
}
[Test]
public void TestDraggingStartingPointRemainsValid()
{
Slider slider = null;
AddStep("Add slider", () =>
{
slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(100, 0)),
};
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
});
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
moveMouse(new Vector2(300));
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
double distanceBefore = 0;
AddStep("store distance", () => distanceBefore = slider.Path.Distance);
moveMouse(new Vector2(300, 300));
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
moveMouse(new Vector2(350, 300));
moveMouse(new Vector2(400, 300));
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
AddAssert("ensure slider still has valid length", () => slider.Path.Distance > 0);
}
[Test]
public void TestDraggingEndingPointRemainsValid()
{
Slider slider = null;
AddStep("Add slider", () =>
{
slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(100, 0)),
};
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
});
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
moveMouse(new Vector2(300));
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
double distanceBefore = 0;
AddStep("store distance", () => distanceBefore = slider.Path.Distance);
moveMouse(new Vector2(400, 300));
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
moveMouse(new Vector2(350, 300));
moveMouse(new Vector2(300, 300));
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
AddAssert("ensure slider still has valid length", () => slider.Path.Distance > 0);
}
/// <summary>
/// If a control point is deleted which results in the slider becoming so short it can't exist,
/// for simplicity delete the slider rather than having it in an invalid state.
///
/// Eventually we may need to change this, based on user feedback. I think it's likely enough of
/// an edge case that we won't get many complaints, though (and there's always the undo button).
/// </summary>
[Test]
public void TestDeletingPointCausesSliderDeletion()
{
AddStep("Add slider", () =>
{
Slider slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(100, 0)),
new PathControlPoint(new Vector2(0, 10))
};
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
});
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
moveMouse(new Vector2(400, 300));
AddStep("delete second point", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Click(MouseButton.Right);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddAssert("ensure object deleted", () => EditorBeatmap.HitObjects.Count == 0);
}
/// <summary>
/// If a scale operation is performed where a single slider is the only thing selected, the path's shape will change.
/// If the scale results in the path becoming too short, further mouse movement in the same direction will not change the shape.
/// </summary>
[Test]
public void TestScalingSliderTooSmallRemainsValid()
{
Slider slider = null;
AddStep("Add slider", () =>
{
slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300, 200) };
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(0, 50)),
new PathControlPoint(new Vector2(0, 100))
};
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
});
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
moveMouse(new Vector2(300));
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
double distanceBefore = 0;
AddStep("store distance", () => distanceBefore = slider.Path.Distance);
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<SelectionBoxDragHandle>().Skip(1).First()));
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
moveMouse(new Vector2(300, 300));
moveMouse(new Vector2(300, 250));
moveMouse(new Vector2(300, 200));
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
AddAssert("ensure slider still has valid length", () => slider.Path.Distance > 0);
}
private void moveMouse(Vector2 pos) =>
AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
}
}

View File

@ -41,9 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addClickStep(MouseButton.Left);
addClickStep(MouseButton.Right);
assertPlaced(true);
assertLength(0);
assertControlPointType(0, PathType.Linear);
assertPlaced(false);
}
[Test]

View File

@ -0,0 +1,72 @@
// 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.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
public class TestSliderScaling : TestSceneOsuEditor
{
private OsuPlayfield playfield;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("get playfield", () => playfield = Editor.ChildrenOfType<OsuPlayfield>().First());
AddStep("seek to first timing point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time));
}
[Test]
public void TestScalingLinearSlider()
{
Slider slider = null;
AddStep("Add slider", () =>
{
slider = new Slider { StartTime = EditorClock.CurrentTime, Position = new Vector2(300) };
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.Linear),
new PathControlPoint(new Vector2(100, 0)),
};
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
});
AddAssert("ensure object placed", () => EditorBeatmap.HitObjects.Count == 1);
moveMouse(new Vector2(300));
AddStep("select slider", () => InputManager.Click(MouseButton.Left));
double distanceBefore = 0;
AddStep("store distance", () => distanceBefore = slider.Path.Distance);
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<SelectionBoxDragHandle>().Skip(1).First()));
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
moveMouse(new Vector2(300, 300));
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("slider length shrunk", () => slider.Path.Distance < distanceBefore);
}
private void moveMouse(Vector2 pos) =>
AddStep($"move mouse to {pos}", () => InputManager.MoveMouseTo(playfield.ToScreenSpace(pos)));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
RelativeSizeAxes = Axes.Both;
}
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
public Drawable GetDrawableComponent(ISkinComponent component) => null;
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
return null;
}
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public ISample GetSample(ISampleInfo sampleInfo) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
public event Action SourceChanged
{

View File

@ -4,13 +4,22 @@
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Testing.Input;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@ -21,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached]
private GameplayBeatmap gameplayBeatmap;
private ClickingCursorContainer lastContainer;
private OsuCursorContainer lastContainer;
[Resolved]
private OsuConfigManager config { get; set; }
@ -48,12 +57,10 @@ namespace osu.Game.Rulesets.Osu.Tests
{
config.SetValue(OsuSetting.AutoCursorSize, true);
gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
Scheduler.AddOnce(recreate);
Scheduler.AddOnce(() => loadContent(false));
});
AddStep("test cursor container", recreate);
void recreate() => SetContents(() => new OsuInputManager(new OsuRuleset().RulesetInfo) { Child = new OsuCursorContainer() });
AddStep("test cursor container", () => loadContent(false));
}
[TestCase(1, 1)]
@ -68,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize);
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
AddStep("load content", loadContent);
AddStep("load content", () => loadContent());
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
@ -82,18 +89,46 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
}
private void loadContent()
[Test]
public void TestTopLeftOrigin()
{
SetContents(() => new MovingCursorInputManager
AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
}
private void loadContent(bool automated = true, Func<SkinProvidingContainer> skinProvider = null)
{
SetContents(() =>
{
Child = lastContainer = new ClickingCursorContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
}
var inputManager = automated ? (InputManager)new MovingCursorInputManager() : new OsuInputManager(new OsuRuleset().RulesetInfo);
var skinContainer = skinProvider?.Invoke() ?? new SkinProvidingContainer(null);
lastContainer = automated ? new ClickingCursorContainer() : new OsuCursorContainer();
return inputManager.WithChild(skinContainer.WithChild(lastContainer));
});
}
private class TopLeftCursorSkin : ISkin
{
public Drawable GetDrawableComponent(ISkinComponent component) => null;
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public ISample GetSample(ISampleInfo sampleInfo) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
switch (lookup)
{
case OsuSkinConfiguration osuLookup:
if (osuLookup == OsuSkinConfiguration.CursorCentre)
return SkinUtils.As<TValue>(new BindableBool(false));
break;
}
return null;
}
}
private class ClickingCursorContainer : OsuCursorContainer
{
private bool pressed;

View File

@ -59,11 +59,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
this.slider = slider;
ControlPoint = controlPoint;
// we don't want to run the path type update on construction as it may inadvertently change the slider.
cachePoints(slider);
slider.Path.Version.BindValueChanged(_ =>
{
PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
cachePoints(slider);
updatePathType();
}, runOnceImmediately: true);
});
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
@ -182,6 +185,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override void OnDrag(DragEvent e)
{
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position.Value).ToArray();
var oldPosition = slider.Position;
var oldStartTime = slider.StartTime;
if (ControlPoint == slider.Path.ControlPoints[0])
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
@ -199,12 +206,24 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
else
ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
if (!slider.Path.HasValidLength)
{
for (var i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position.Value = oldControlPoints[i];
slider.Position = oldPosition;
slider.StartTime = oldStartTime;
return;
}
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
PointsInSegment[0].Type.Value = dragPathType;
}
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
/// <summary>
/// Handles correction of invalid path types.
/// </summary>

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private InputManager inputManager;
private PlacementState state;
private SliderPlacementState state;
private PathControlPoint segmentStart;
private PathControlPoint cursor;
private int currentSegmentLength;
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPointVisualiser = new PathControlPointVisualiser(HitObject, false)
};
setState(PlacementState.Initial);
setState(SliderPlacementState.Initial);
}
protected override void LoadComplete()
@ -73,12 +73,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
switch (state)
{
case PlacementState.Initial:
case SliderPlacementState.Initial:
BeginPlacement();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break;
case PlacementState.Body:
case SliderPlacementState.Body:
updateCursor();
break;
}
@ -91,11 +91,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
switch (state)
{
case PlacementState.Initial:
case SliderPlacementState.Initial:
beginCurve();
break;
case PlacementState.Body:
case SliderPlacementState.Body:
if (canPlaceNewControlPoint(out var lastPoint))
{
// Place a new point by detatching the current cursor.
@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void OnMouseUp(MouseUpEvent e)
{
if (state == PlacementState.Body && e.Button == MouseButton.Right)
if (state == SliderPlacementState.Body && e.Button == MouseButton.Right)
endCurve();
base.OnMouseUp(e);
}
@ -129,13 +129,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void beginCurve()
{
BeginPlacement(commitStart: true);
setState(PlacementState.Body);
setState(SliderPlacementState.Body);
}
private void endCurve()
{
updateSlider();
EndPlacement(true);
EndPlacement(HitObject.Path.HasValidLength);
}
protected override void Update()
@ -219,12 +219,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
tailCirclePiece.UpdateFrom(HitObject.TailCircle);
}
private void setState(PlacementState newState)
private void setState(SliderPlacementState newState)
{
state = newState;
}
private enum PlacementState
private enum SliderPlacementState
{
Initial,
Body,

View File

@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted
if (controlPoints.Count <= 1)
if (controlPoints.Count <= 1 || !slider.HitObject.Path.HasValidLength)
{
placementHandler?.Delete(HitObject);
return;

View File

@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckOffscreenObjects : ICheck
{
// A close approximation for the bounding box of the screen in gameplay on 4:3 aspect ratio.
// Uses gameplay space coordinates (512 x 384 playfield / 640 x 480 screen area).
// See https://github.com/ppy/osu/pull/12361#discussion_r612199777 for reference.
private const int min_x = -67;
private const int min_y = -60;
private const int max_x = 579;
private const int max_y = 428;
// The amount of milliseconds to step through a slider path at a time
// (higher = more performant, but higher false-negative chance).
private const int path_step_size = 5;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Offscreen hitobjects");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateOffscreenCircle(this),
new IssueTemplateOffscreenSlider(this)
};
public IEnumerable<Issue> Run(IBeatmap beatmap)
{
foreach (var hitobject in beatmap.HitObjects)
{
switch (hitobject)
{
case Slider slider:
{
foreach (var issue in sliderIssues(slider))
yield return issue;
break;
}
case HitCircle circle:
{
if (isOffscreen(circle.StackedPosition, circle.Radius))
yield return new IssueTemplateOffscreenCircle(this).Create(circle);
break;
}
}
}
}
/// <summary>
/// Steps through points on the slider to ensure the entire path is on-screen.
/// Returns at most one issue.
/// </summary>
/// <param name="slider">The slider whose path to check.</param>
/// <returns></returns>
private IEnumerable<Issue> sliderIssues(Slider slider)
{
for (int i = 0; i < slider.Distance; i += path_step_size)
{
double progress = i / slider.Distance;
Vector2 position = slider.StackedPositionAt(progress);
if (!isOffscreen(position, slider.Radius))
continue;
// `SpanDuration` ensures we don't include reverses.
double time = slider.StartTime + progress * slider.SpanDuration;
yield return new IssueTemplateOffscreenSlider(this).Create(slider, time);
yield break;
}
// Above loop may skip the last position in the slider due to step size.
if (!isOffscreen(slider.StackedEndPosition, slider.Radius))
yield break;
yield return new IssueTemplateOffscreenSlider(this).Create(slider, slider.EndTime);
}
private bool isOffscreen(Vector2 position, double radius)
{
return position.X - radius < min_x || position.X + radius > max_x ||
position.Y - radius < min_y || position.Y + radius > max_y;
}
public class IssueTemplateOffscreenCircle : IssueTemplate
{
public IssueTemplateOffscreenCircle(ICheck check)
: base(check, IssueType.Problem, "This circle goes offscreen on a 4:3 aspect ratio.")
{
}
public Issue Create(HitCircle circle) => new Issue(circle, this);
}
public class IssueTemplateOffscreenSlider : IssueTemplate
{
public IssueTemplateOffscreenSlider(ICheck check)
: base(check, IssueType.Problem, "This slider goes offscreen here on a 4:3 aspect ratio.")
{
}
public Issue Create(Slider slider, double offscreenTime) => new Issue(slider, this) { Time = offscreenTime };
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Edit.Checks;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuBeatmapVerifier : IBeatmapVerifier
{
private readonly List<ICheck> checks = new List<ICheck>
{
new CheckOffscreenObjects()
};
public IEnumerable<Issue> Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap));
}
}

View File

@ -205,10 +205,12 @@ namespace osu.Game.Rulesets.Osu.Edit
Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / sliderQuad.Width, 1 + scale.Y / sliderQuad.Height);
Vector2 pathRelativeDeltaScale = new Vector2(
sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
Queue<Vector2> oldControlPoints = new Queue<Vector2>();
@ -226,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Edit
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
if (xInBounds && yInBounds)
if (xInBounds && yInBounds && slider.Path.HasValidLength)
return;
foreach (var point in slider.Path.ControlPoints)

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModBarrelRoll : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
[SettingSource("Roll speed", "Rotations per minute")]
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
{
MinValue = 0.02,
MaxValue = 4,
Precision = 0.01,
};
[SettingSource("Direction", "The direction of rotation")]
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
public override string Name => "Barrel Roll";
public override string Acronym => "BR";
public override string Description => "The whole playfield is on a wheel!";
public override double ScoreMultiplier => 1;
public void Update(Playfield playfield)
{
playfield.Rotation = (Direction.Value == RotationDirection.CounterClockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// scale the playfield to allow all hitobjects to stay within the visible region.
drawableRuleset.Playfield.Scale = new Vector2(OsuPlayfield.BASE_SIZE.Y / OsuPlayfield.BASE_SIZE.X);
}
}
}

View File

@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Touch Device";
public override string Acronym => "TD";
public override string Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System;

View File

@ -185,6 +185,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModGrow(), new OsuModDeflate()),
new MultiMod(new ModWindUp(), new ModWindDown()),
new OsuModTraceable(),
new OsuModBarrelRoll(),
};
case ModType.System:
@ -206,6 +207,8 @@ namespace osu.Game.Rulesets.Osu
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
public override IBeatmapVerifier CreateBeatmapVerifier() => new OsuBeatmapVerifier();
public override string Description => "osu!";
public override string ShortName => SHORT_NAME;

View File

@ -71,8 +71,6 @@ namespace osu.Game.Rulesets.Osu.Replays
buttonIndex = 0;
AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500)));
AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500)));
AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500)));
for (int i = 0; i < Beatmap.HitObjects.Count; i++)

View File

@ -2,13 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Input.StateChanges;
using osu.Framework.Utils;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Replays
{
@ -21,24 +19,11 @@ namespace osu.Game.Rulesets.Osu.Replays
protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any();
protected Vector2? Position
{
get
{
var frame = CurrentFrame;
if (frame == null)
return null;
Debug.Assert(CurrentTime != null);
return NextFrame != null ? Interpolation.ValueAt(CurrentTime.Value, frame.Position, NextFrame.Position, frame.Time, NextFrame.Time) : frame.Position;
}
}
public override void CollectPendingInputs(List<IInput> inputs)
{
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) });
var position = Interpolation.ValueAt(CurrentTime, StartFrame.Position, EndFrame.Position, StartFrame.Time, EndFrame.Time);
inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(position) });
inputs.Add(new ReplayState<OsuAction> { PressedActions = CurrentFrame?.Actions ?? new List<OsuAction>() });
}
}

View File

@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
spin = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorRotate)?.Value ?? true;
InternalChildren = new[]
@ -32,13 +33,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Texture = skin.GetTexture("cursor"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Origin = centre ? Anchor.Centre : Anchor.TopLeft,
},
new NonPlayfieldSprite
{
Texture = skin.GetTexture("cursormiddle"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Origin = centre ? Anchor.Centre : Anchor.TopLeft,
},
};
}

View File

@ -26,7 +26,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
Blending = !disjointTrail ? BlendingParameters.Additive : BlendingParameters.Inherit;
if (disjointTrail)
{
bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
TrailOrigin = centre ? Anchor.Centre : Anchor.TopLeft;
Blending = BlendingParameters.Inherit;
}
else
{
Blending = BlendingParameters.Additive;
}
if (Texture != null)
{

View File

@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
SliderBorderSize,
SliderPathRadius,
AllowSliderBallTint,
CursorCentre,
CursorExpand,
CursorRotate,
HitCircleOverlayAboveNumber,

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Vertices;
@ -31,6 +32,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private double timeOffset;
private float time;
private Anchor trailOrigin = Anchor.Centre;
protected Anchor TrailOrigin
{
get => trailOrigin;
set
{
trailOrigin = value;
Invalidate(Invalidation.DrawNode);
}
}
public CursorTrail()
{
// as we are currently very dependent on having a running clock, let's make our own clock for the time being.
@ -197,6 +210,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size;
private Vector2 originPosition;
private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1);
public TrailDrawNode(CursorTrail source)
@ -213,6 +228,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
size = Source.partSize;
time = Source.time;
originPosition = Vector2.Zero;
if (Source.TrailOrigin.HasFlagFast(Anchor.x1))
originPosition.X = 0.5f;
else if (Source.TrailOrigin.HasFlagFast(Anchor.x2))
originPosition.X = 1f;
if (Source.TrailOrigin.HasFlagFast(Anchor.y1))
originPosition.Y = 0.5f;
else if (Source.TrailOrigin.HasFlagFast(Anchor.y2))
originPosition.Y = 1f;
Source.parts.CopyTo(parts, 0);
}
@ -237,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y + size.Y / 2),
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
@ -246,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y + size.Y / 2),
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear,
@ -255,7 +282,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y - size.Y / 2),
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear,
@ -264,7 +291,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y - size.Y / 2),
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear,

View File

@ -42,6 +42,9 @@ namespace osu.Game.Rulesets.Osu.UI
public OsuPlayfield()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },

View File

@ -33,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.UI
{
Add(cursorScaleContainer = new Container
{
RelativePositionAxes = Axes.Both,
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
});
}

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
{
base.UpdateTimeAndPosition(result);
if (PlacementActive)
if (PlacementActive == PlacementState.Active)
{
if (result.Time is double dragTime)
{

View File

@ -35,7 +35,6 @@ namespace osu.Game.Rulesets.Taiko.Replays
bool hitButton = true;
Frames.Add(new TaikoReplayFrame(-100000));
Frames.Add(new TaikoReplayFrame(Beatmap.HitObjects[0].StartTime - 1000));
for (int i = 0; i < Beatmap.HitObjects.Count; i++)

View File

@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckBackgroundTest
{
private CheckBackground check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckBackground();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo { Filename = "abc123.jpg" }
})
}
}
};
}
[Test]
public void TestBackgroundSetAndInFiles()
{
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestBackgroundSetAndNotInFiles()
{
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
var issues = check.Run(beatmap).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateDoesNotExist);
}
[Test]
public void TestBackgroundNotSet()
{
beatmap.Metadata.BackgroundFile = null;
var issues = check.Run(beatmap).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateNoneSet);
}
}
}

View File

@ -144,6 +144,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => nameof(ModA);
public override string Acronym => nameof(ModA);
public override string Description => string.Empty;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) };
@ -152,6 +153,7 @@ namespace osu.Game.Tests.NonVisual
private class ModB : Mod
{
public override string Name => nameof(ModB);
public override string Description => string.Empty;
public override string Acronym => nameof(ModB);
public override double ScoreMultiplier => 1;
@ -162,6 +164,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => nameof(ModC);
public override string Acronym => nameof(ModC);
public override string Description => string.Empty;
public override double ScoreMultiplier => 1;
}
@ -169,6 +172,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => $"Incompatible With {nameof(ModA)}";
public override string Acronym => $"Incompatible With {nameof(ModA)}";
public override string Description => string.Empty;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModA) };
@ -187,6 +191,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
public override string Acronym => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
public override string Description => string.Empty;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };

View File

@ -20,27 +20,14 @@ namespace osu.Game.Tests.NonVisual
{
handler = new TestInputHandler(replay = new Replay
{
Frames = new List<ReplayFrame>
{
new TestReplayFrame(0),
new TestReplayFrame(1000),
new TestReplayFrame(2000),
new TestReplayFrame(3000, true),
new TestReplayFrame(4000, true),
new TestReplayFrame(5000, true),
new TestReplayFrame(7000, true),
new TestReplayFrame(8000),
}
HasReceivedAllFrames = false
});
}
[Test]
public void TestNormalPlayback()
{
Assert.IsNull(handler.CurrentFrame);
confirmCurrentFrame(null);
confirmNextFrame(0);
setReplayFrames();
setTime(0, 0);
confirmCurrentFrame(0);
@ -107,6 +94,8 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestIntroTime()
{
setReplayFrames();
setTime(-1000, -1000);
confirmCurrentFrame(null);
confirmNextFrame(0);
@ -123,6 +112,8 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestBasicRewind()
{
setReplayFrames();
setTime(2800, 0);
setTime(2800, 1000);
setTime(2800, 2000);
@ -133,34 +124,35 @@ namespace osu.Game.Tests.NonVisual
// pivot without crossing a frame boundary
setTime(2700, 2700);
confirmCurrentFrame(2);
confirmNextFrame(1);
confirmNextFrame(3);
// cross current frame boundary; should not yet update frame
setTime(1980, 1980);
// cross current frame boundary
setTime(1980, 2000);
confirmCurrentFrame(2);
confirmNextFrame(1);
confirmNextFrame(3);
setTime(1200, 1200);
confirmCurrentFrame(2);
confirmNextFrame(1);
confirmCurrentFrame(1);
confirmNextFrame(2);
// ensure each frame plays out until start
setTime(-500, 1000);
confirmCurrentFrame(1);
confirmNextFrame(0);
confirmNextFrame(2);
setTime(-500, 0);
confirmCurrentFrame(0);
confirmNextFrame(null);
confirmNextFrame(1);
setTime(-500, -500);
confirmCurrentFrame(0);
confirmNextFrame(null);
confirmCurrentFrame(null);
confirmNextFrame(0);
}
[Test]
public void TestRewindInsideImportantSection()
{
setReplayFrames();
fastForwardToPoint(3000);
setTime(4000, 4000);
@ -168,12 +160,12 @@ namespace osu.Game.Tests.NonVisual
confirmNextFrame(5);
setTime(3500, null);
confirmCurrentFrame(4);
confirmNextFrame(3);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(3000, 3000);
confirmCurrentFrame(3);
confirmNextFrame(2);
confirmNextFrame(4);
setTime(3500, null);
confirmCurrentFrame(3);
@ -187,46 +179,127 @@ namespace osu.Game.Tests.NonVisual
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(4000, null);
setTime(4000, 4000);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(3500, null);
confirmCurrentFrame(4);
confirmNextFrame(3);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(3000, 3000);
confirmCurrentFrame(3);
confirmNextFrame(2);
confirmNextFrame(4);
}
[Test]
public void TestRewindOutOfImportantSection()
{
setReplayFrames();
fastForwardToPoint(3500);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(3200, null);
// next frame doesn't change even though direction reversed, because of important section.
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(3000, null);
setTime(3000, 3000);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(2800, 2800);
confirmCurrentFrame(3);
confirmNextFrame(2);
confirmCurrentFrame(2);
confirmNextFrame(3);
}
[Test]
public void TestReplayStreaming()
{
// no frames are arrived yet
setTime(0, null);
setTime(1000, null);
Assert.IsTrue(handler.WaitingForFrame, "Should be waiting for the first frame");
replay.Frames.Add(new TestReplayFrame(0));
replay.Frames.Add(new TestReplayFrame(1000));
// should always play from beginning
setTime(1000, 0);
confirmCurrentFrame(0);
Assert.IsFalse(handler.WaitingForFrame, "Should not be waiting yet");
setTime(1000, 1000);
confirmCurrentFrame(1);
confirmNextFrame(null);
Assert.IsTrue(handler.WaitingForFrame, "Should be waiting");
// cannot seek beyond the last frame
setTime(1500, null);
confirmCurrentFrame(1);
setTime(-100, 0);
confirmCurrentFrame(0);
// can seek to the point before the first frame, however
setTime(-100, -100);
confirmCurrentFrame(null);
confirmNextFrame(0);
fastForwardToPoint(1000);
setTime(3000, null);
replay.Frames.Add(new TestReplayFrame(2000));
confirmCurrentFrame(1);
setTime(1000, 1000);
setTime(3000, 2000);
}
[Test]
public void TestMultipleFramesSameTime()
{
replay.Frames.Add(new TestReplayFrame(0));
replay.Frames.Add(new TestReplayFrame(0));
replay.Frames.Add(new TestReplayFrame(1000));
replay.Frames.Add(new TestReplayFrame(1000));
replay.Frames.Add(new TestReplayFrame(2000));
// forward direction is prioritized when multiple frames have the same time.
setTime(0, 0);
setTime(0, 0);
setTime(2000, 1000);
setTime(2000, 1000);
setTime(1000, 1000);
setTime(1000, 1000);
setTime(-100, 1000);
setTime(-100, 0);
setTime(-100, 0);
setTime(-100, -100);
}
private void setReplayFrames()
{
replay.Frames = new List<ReplayFrame>
{
new TestReplayFrame(0),
new TestReplayFrame(1000),
new TestReplayFrame(2000),
new TestReplayFrame(3000, true),
new TestReplayFrame(4000, true),
new TestReplayFrame(5000, true),
new TestReplayFrame(7000, true),
new TestReplayFrame(8000),
};
replay.HasReceivedAllFrames = true;
}
private void fastForwardToPoint(double destination)
{
for (int i = 0; i < 1000; i++)
{
if (handler.SetFrameFromTime(destination) == null)
var time = handler.SetFrameFromTime(destination);
if (time == null || time == destination)
return;
}
@ -235,33 +308,17 @@ namespace osu.Game.Tests.NonVisual
private void setTime(double set, double? expect)
{
Assert.AreEqual(expect, handler.SetFrameFromTime(set));
Assert.AreEqual(expect, handler.SetFrameFromTime(set), "Unexpected return value");
}
private void confirmCurrentFrame(int? frame)
{
if (frame.HasValue)
{
Assert.IsNotNull(handler.CurrentFrame);
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
}
else
{
Assert.IsNull(handler.CurrentFrame);
}
Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.CurrentFrame?.Time, "Unexpected current frame");
}
private void confirmNextFrame(int? frame)
{
if (frame.HasValue)
{
Assert.IsNotNull(handler.NextFrame);
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
}
else
{
Assert.IsNull(handler.NextFrame);
}
Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.NextFrame?.Time, "Unexpected next frame");
}
private class TestReplayFrame : ReplayFrame

View File

@ -1,296 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class StreamingFramedReplayInputHandlerTest
{
private Replay replay;
private TestInputHandler handler;
[SetUp]
public void SetUp()
{
handler = new TestInputHandler(replay = new Replay
{
HasReceivedAllFrames = false,
Frames = new List<ReplayFrame>
{
new TestReplayFrame(0),
new TestReplayFrame(1000),
new TestReplayFrame(2000),
new TestReplayFrame(3000, true),
new TestReplayFrame(4000, true),
new TestReplayFrame(5000, true),
new TestReplayFrame(7000, true),
new TestReplayFrame(8000),
}
});
}
[Test]
public void TestNormalPlayback()
{
Assert.IsNull(handler.CurrentFrame);
confirmCurrentFrame(null);
confirmNextFrame(0);
setTime(0, 0);
confirmCurrentFrame(0);
confirmNextFrame(1);
// if we hit the first frame perfectly, time should progress to it.
setTime(1000, 1000);
confirmCurrentFrame(1);
confirmNextFrame(2);
// in between non-important frames should progress based on input.
setTime(1200, 1200);
confirmCurrentFrame(1);
setTime(1400, 1400);
confirmCurrentFrame(1);
// progressing beyond the next frame should force time to that frame once.
setTime(2200, 2000);
confirmCurrentFrame(2);
// second attempt should progress to input time
setTime(2200, 2200);
confirmCurrentFrame(2);
// entering important section
setTime(3000, 3000);
confirmCurrentFrame(3);
// cannot progress within
setTime(3500, null);
confirmCurrentFrame(3);
setTime(4000, 4000);
confirmCurrentFrame(4);
// still cannot progress
setTime(4500, null);
confirmCurrentFrame(4);
setTime(5200, 5000);
confirmCurrentFrame(5);
// important section AllowedImportantTimeSpan allowance
setTime(5200, 5200);
confirmCurrentFrame(5);
setTime(7200, 7000);
confirmCurrentFrame(6);
setTime(7200, null);
confirmCurrentFrame(6);
// exited important section
setTime(8200, 8000);
confirmCurrentFrame(7);
confirmNextFrame(null);
setTime(8200, null);
confirmCurrentFrame(7);
confirmNextFrame(null);
setTime(8400, null);
confirmCurrentFrame(7);
confirmNextFrame(null);
}
[Test]
public void TestIntroTime()
{
setTime(-1000, -1000);
confirmCurrentFrame(null);
confirmNextFrame(0);
setTime(-500, -500);
confirmCurrentFrame(null);
confirmNextFrame(0);
setTime(0, 0);
confirmCurrentFrame(0);
confirmNextFrame(1);
}
[Test]
public void TestBasicRewind()
{
setTime(2800, 0);
setTime(2800, 1000);
setTime(2800, 2000);
setTime(2800, 2800);
confirmCurrentFrame(2);
confirmNextFrame(3);
// pivot without crossing a frame boundary
setTime(2700, 2700);
confirmCurrentFrame(2);
confirmNextFrame(1);
// cross current frame boundary; should not yet update frame
setTime(1980, 1980);
confirmCurrentFrame(2);
confirmNextFrame(1);
setTime(1200, 1200);
confirmCurrentFrame(2);
confirmNextFrame(1);
// ensure each frame plays out until start
setTime(-500, 1000);
confirmCurrentFrame(1);
confirmNextFrame(0);
setTime(-500, 0);
confirmCurrentFrame(0);
confirmNextFrame(null);
setTime(-500, -500);
confirmCurrentFrame(0);
confirmNextFrame(null);
}
[Test]
public void TestRewindInsideImportantSection()
{
fastForwardToPoint(3000);
setTime(4000, 4000);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(3500, null);
confirmCurrentFrame(4);
confirmNextFrame(3);
setTime(3000, 3000);
confirmCurrentFrame(3);
confirmNextFrame(2);
setTime(3500, null);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(4000, 4000);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(4500, null);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(4000, null);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(3500, null);
confirmCurrentFrame(4);
confirmNextFrame(3);
setTime(3000, 3000);
confirmCurrentFrame(3);
confirmNextFrame(2);
}
[Test]
public void TestRewindOutOfImportantSection()
{
fastForwardToPoint(3500);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(3200, null);
// next frame doesn't change even though direction reversed, because of important section.
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(3000, null);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(2800, 2800);
confirmCurrentFrame(3);
confirmNextFrame(2);
}
private void fastForwardToPoint(double destination)
{
for (int i = 0; i < 1000; i++)
{
if (handler.SetFrameFromTime(destination) == null)
return;
}
throw new TimeoutException("Seek was never fulfilled");
}
private void setTime(double set, double? expect)
{
Assert.AreEqual(expect, handler.SetFrameFromTime(set));
}
private void confirmCurrentFrame(int? frame)
{
if (frame.HasValue)
{
Assert.IsNotNull(handler.CurrentFrame);
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
}
else
{
Assert.IsNull(handler.CurrentFrame);
}
}
private void confirmNextFrame(int? frame)
{
if (frame.HasValue)
{
Assert.IsNotNull(handler.NextFrame);
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
}
else
{
Assert.IsNull(handler.NextFrame);
}
}
private class TestReplayFrame : ReplayFrame
{
public readonly bool IsImportant;
public TestReplayFrame(double time, bool isImportant = false)
: base(time)
{
IsImportant = isImportant;
}
}
private class TestInputHandler : FramedReplayInputHandler<TestReplayFrame>
{
public TestInputHandler(Replay replay)
: base(replay)
{
FrameAccuratePlayback = true;
}
protected override double AllowedImportantTimeSpan => 1000;
protected override bool IsImportant(TestReplayFrame frame) => frame.IsImportant;
}
}
}

View File

@ -140,6 +140,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
@ -156,6 +157,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]

View File

@ -100,6 +100,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
@ -116,6 +117,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
@ -150,6 +152,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]

View File

@ -65,6 +65,21 @@ namespace osu.Game.Tests.Visual.Background
stack.Push(songSelect = new DummySongSelect());
});
/// <summary>
/// User settings should always be ignored on song select screen.
/// </summary>
[Test]
public void TestUserSettingsIgnoredOnSongSelect()
{
setupUserSettings();
AddUntilStep("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
AddUntilStep("Screen using background blur", () => songSelect.IsBackgroundBlur());
performFullSetup();
AddStep("Exit to song select", () => player.Exit());
AddUntilStep("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
AddUntilStep("Screen using background blur", () => songSelect.IsBackgroundBlur());
}
/// <summary>
/// Check if <see cref="PlayerLoader"/> properly triggers the visual settings preview when a user hovers over the visual settings panel.
/// </summary>
@ -142,9 +157,9 @@ namespace osu.Game.Tests.Visual.Background
{
performFullSetup();
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false);
AddStep("Disable user dim", () => songSelect.IgnoreUserSettings.Value = true);
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true);
AddStep("Enable user dim", () => songSelect.IgnoreUserSettings.Value = false);
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
}
@ -161,13 +176,36 @@ namespace osu.Game.Tests.Visual.Background
player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true);
AddStep("Enable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = false);
AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f);
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false);
AddStep("Disable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true);
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
}
[Test]
public void TestStoryboardIgnoreUserSettings()
{
performFullSetup();
createFakeStoryboard();
AddStep("Enable replacing background", () => player.ReplacesBackground.Value = true);
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
AddStep("Ignore user settings", () =>
{
player.ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
});
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
AddUntilStep("Background is invisible", () => songSelect.IsBackgroundInvisible());
AddStep("Disable background replacement", () => player.ReplacesBackground.Value = false);
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
}
/// <summary>
/// Check if the visual settings container retains dim and blur when pausing
/// </summary>
@ -204,17 +242,6 @@ namespace osu.Game.Tests.Visual.Background
songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && songSelect.CheckBackgroundBlur(results.ExpectedBackgroundBlur));
}
/// <summary>
/// Check if background gets undimmed and unblurred when leaving <see cref="Player"/> for <see cref="PlaySongSelect"/>
/// </summary>
[Test]
public void TestTransitionOut()
{
performFullSetup();
AddStep("Exit to song select", () => player.Exit());
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect());
}
/// <summary>
/// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim.
/// </summary>
@ -281,11 +308,11 @@ namespace osu.Game.Tests.Visual.Background
protected override BackgroundScreen CreateBackground()
{
background = new FadeAccessibleBackground(Beatmap.Value);
DimEnabled.BindTo(background.EnableUserDim);
IgnoreUserSettings.BindTo(background.IgnoreUserSettings);
return background;
}
public readonly Bindable<bool> DimEnabled = new Bindable<bool>();
public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>();
public readonly Bindable<double> DimLevel = new BindableDouble();
public readonly Bindable<double> BlurLevel = new BindableDouble();
@ -310,7 +337,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
public bool IsBlurCorrect() => background.CurrentBlur == new Vector2(BACKGROUND_BLUR);
public bool IsBackgroundBlur() => background.CurrentBlur == new Vector2(BACKGROUND_BLUR);
public bool CheckBackgroundBlur(Vector2 expected) => background.CurrentBlur == expected;

View File

@ -1,88 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorQuickDelete : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First();
[Test]
public void TestQuickDeleteRemovesObject()
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("move mouse to object", () =>
{
var pos = blueprintContainer.ChildrenOfType<HitCirclePiece>().First().ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
}
[Test]
public void TestQuickDeleteRemovesSliderControlPoint()
{
Slider slider = new Slider { StartTime = 1000 };
PathControlPoint[] points =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(50, 0)),
new PathControlPoint(new Vector2(100, 0))
};
AddStep("add slider", () =>
{
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
});
AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to controlpoint", () =>
{
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2);
// second click should nuke the object completely.
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
}
}

View File

@ -0,0 +1,219 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorSelection : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First();
private void moveMouseToObject(Func<HitObject> targetFunc)
{
AddStep("move mouse to object", () =>
{
var pos = blueprintContainer.SelectionBlueprints
.First(s => s.HitObject == targetFunc())
.ChildrenOfType<HitCirclePiece>()
.First().ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
}
[Test]
public void TestBasicSelect()
{
var addedObject = new HitCircle { StartTime = 100 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
moveMouseToObject(() => addedObject);
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
var addedObject2 = new HitCircle
{
StartTime = 100,
Position = new Vector2(100),
};
AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2));
AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
moveMouseToObject(() => addedObject2);
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2);
}
[Test]
public void TestMultiSelect()
{
var addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) },
new HitCircle { StartTime = 300, Position = new Vector2(100) },
new HitCircle { StartTime = 400, Position = new Vector2(150) },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
moveMouseToObject(() => addedObjects[0]);
AddStep("click first", () => InputManager.Click(MouseButton.Left));
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]);
AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
moveMouseToObject(() => addedObjects[1]);
AddStep("click second", () => InputManager.Click(MouseButton.Left));
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
moveMouseToObject(() => addedObjects[2]);
AddStep("click third", () => InputManager.Click(MouseButton.Left));
AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2]));
moveMouseToObject(() => addedObjects[1]);
AddStep("click second", () => InputManager.Click(MouseButton.Left));
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
}
[TestCase(false)]
[TestCase(true)]
public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag)
{
HitCircle[] addedObjects = null;
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) },
new HitCircle { StartTime = 300, Position = new Vector2(100) },
new HitCircle { StartTime = 400, Position = new Vector2(150) },
}));
moveMouseToObject(() => addedObjects[0]);
AddStep("click first", () => InputManager.Click(MouseButton.Left));
AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
moveMouseToObject(() => addedObjects[1]);
if (alreadySelectedBeforeDrag)
AddStep("click second", () => InputManager.Click(MouseButton.Left));
AddStep("mouse down on second", () => InputManager.PressButton(MouseButton.Left));
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
AddStep("drag to centre", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre));
AddAssert("positions changed", () => addedObjects[0].Position != Vector2.Zero && addedObjects[1].Position != new Vector2(50));
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left));
}
[Test]
public void TestBasicDeselect()
{
var addedObject = new HitCircle { StartTime = 100 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
moveMouseToObject(() => addedObject);
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
AddStep("click away", () =>
{
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
});
AddAssert("selection lost", () => EditorBeatmap.SelectedHitObjects.Count == 0);
}
[Test]
public void TestQuickDeleteRemovesObject()
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
moveMouseToObject(() => addedObject);
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
}
[Test]
public void TestQuickDeleteRemovesSliderControlPoint()
{
Slider slider = null;
PathControlPoint[] points =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(50, 0)),
new PathControlPoint(new Vector2(100, 0))
};
AddStep("add slider", () =>
{
slider = new Slider
{
StartTime = 1000,
Path = new SliderPath(points)
};
EditorBeatmap.Add(slider);
});
AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to controlpoint", () =>
{
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2);
AddStep("right click", () => InputManager.Click(MouseButton.Right));
// second click should nuke the object completely.
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
}
}

View File

@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osuTK;
@ -16,18 +15,28 @@ namespace osu.Game.Tests.Visual.Editing
public class TestSceneEditorSummaryTimeline : EditorClockTestScene
{
[Cached(typeof(EditorBeatmap))]
private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap());
private readonly EditorBeatmap editorBeatmap;
[BackgroundDependencyLoader]
private void load()
public TestSceneEditorSummaryTimeline()
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
}
Add(new SummaryTimeline
protected override void LoadComplete()
{
base.LoadComplete();
AddStep("create timeline", () =>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 50)
// required for track
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Add(new SummaryTimeline
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 50)
});
});
}
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestDisallowZeroDurationObjects()
{
DragBar dragBar;
DragArea dragArea;
AddStep("add spinner", () =>
{
@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Editing
EditorBeatmap.Add(new Spinner
{
Position = new Vector2(256, 256),
StartTime = 150,
StartTime = 2700,
Duration = 500
});
});
@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("hold down drag bar", () =>
{
// distinguishes between the actual drag bar and its "underlay shadow".
dragBar = this.ChildrenOfType<DragBar>().Single(bar => bar.HandlePositionalInput);
InputManager.MoveMouseTo(dragBar);
dragArea = this.ChildrenOfType<DragArea>().Single(bar => bar.HandlePositionalInput);
InputManager.MoveMouseTo(dragArea);
InputManager.PressButton(MouseButton.Left);
});

View File

@ -53,13 +53,10 @@ namespace osu.Game.Tests.Visual.Editing
new AudioVisualiser(),
}
},
TimelineArea = new TimelineArea
TimelineArea = new TimelineArea(CreateTestComponent())
{
Child = CreateTestComponent(),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Size = new Vector2(0.8f, 100),
}
});
}

View File

@ -25,6 +25,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@ -48,6 +49,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private readonly VolumeOverlay volumeOverlay;
[Cached(typeof(BatteryInfo))]
private readonly LocalBatteryInfo batteryInfo = new LocalBatteryInfo();
private readonly ChangelogOverlay changelogOverlay;
public TestScenePlayerLoader()
@ -288,6 +292,33 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
[TestCase(false, 1.0, false)] // not charging, above cutoff --> no warning
[TestCase(true, 0.1, false)] // charging, below cutoff --> no warning
[TestCase(false, 0.25, true)] // not charging, at cutoff --> warning
public void TestLowBatteryNotification(bool isCharging, double chargeLevel, bool shouldWarn)
{
AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce).Value = false);
// set charge status and level
AddStep("load player", () => resetPlayer(false, () =>
{
batteryInfo.SetCharging(isCharging);
batteryInfo.SetChargeLevel(chargeLevel);
}));
AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
AddAssert($"notification {(shouldWarn ? "triggered" : "not triggered")}", () => notificationOverlay.UnreadCount.Value == (shouldWarn ? 1 : 0));
AddStep("click notification", () =>
{
var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last();
var flowContainer = scrollContainer.Children.OfType<FillFlowContainer<NotificationSection>>().First();
var notification = flowContainer.First();
InputManager.MoveMouseTo(notification);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for player load", () => player.IsLoaded);
}
[Test]
public void TestEpilepsyWarningEarlyExit()
{
@ -321,6 +352,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override string Name => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
public override string Description => string.Empty;
public bool Applied { get; private set; }
@ -348,5 +380,29 @@ namespace osu.Game.Tests.Visual.Gameplay
throw new TimeoutException();
}
}
/// <summary>
/// Mutable dummy BatteryInfo class for <see cref="TestScenePlayerLoader.TestLowBatteryNotification"/>
/// </summary>
/// <inheritdoc/>
private class LocalBatteryInfo : BatteryInfo
{
private bool isCharging = true;
private double chargeLevel = 1;
public override bool IsCharging => isCharging;
public override double ChargeLevel => chargeLevel;
public void SetCharging(bool value)
{
isCharging = value;
}
public void SetChargeLevel(double value)
{
chargeLevel = value;
}
}
}
}

View File

@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
waitForPlayer();
AddAssert("ensure frames arrived", () => replayHandler.HasFrames);
AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null);
AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame);
checkPaused(true);
double? pausedTime = null;
@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay
sendFrames();
AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null);
AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame);
checkPaused(true);
AddAssert("time advanced", () => currentFrameStableTime > pausedTime);

View File

@ -204,27 +204,27 @@ namespace osu.Game.Tests.Visual.Gameplay
return;
}
if (replayHandler.NextFrame != null)
{
var lastFrame = replay.Frames.LastOrDefault();
if (!replayHandler.HasFrames)
return;
// this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved).
// in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation.
if (lastFrame != null)
latency = Math.Max(latency, Time.Current - lastFrame.Time);
var lastFrame = replay.Frames.LastOrDefault();
latencyDisplay.Text = $"latency: {latency:N1}";
// this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved).
// in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation.
if (lastFrame != null)
latency = Math.Max(latency, Time.Current - lastFrame.Time);
double proposedTime = Time.Current - latency + Time.Elapsed;
latencyDisplay.Text = $"latency: {latency:N1}";
// this will either advance by one or zero frames.
double? time = replayHandler.SetFrameFromTime(proposedTime);
double proposedTime = Time.Current - latency + Time.Elapsed;
if (time == null)
return;
// this will either advance by one or zero frames.
double? time = replayHandler.SetFrameFromTime(proposedTime);
manualClock.CurrentTime = time.Value;
}
if (time == null)
return;
manualClock.CurrentTime = time.Value;
}
[TearDownSteps]

View File

@ -13,6 +13,7 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Beatmaps;
@ -128,6 +129,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().ChildrenOfType<ReadyButton>().Single().Enabled.Value);
AddStep("click ready button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerReadyButton>().Single());

View File

@ -57,6 +57,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private abstract class TestMod : Mod, IApplicableMod
{
public override double ScoreMultiplier => 1.0;
public override string Description => "This is a test mod.";
}
}
}

View File

@ -226,6 +226,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public override double ScoreMultiplier => 1.0;
public override string Description => "This is a customisable test mod.";
public override ModType Type => ModType.Conversion;
[SettingSource("Sample float", "Change something for a mod")]

View File

@ -22,4 +22,4 @@
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.ControlPoints
MaxValue = 10
};
public override Color4 GetRepresentingColour(OsuColour colours) => colours.GreenDark;
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
/// <summary>
/// The speed multiplier at this control point.

View File

@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
private const double default_beat_length = 60000.0 / 60.0;
public override Color4 GetRepresentingColour(OsuColour colours) => colours.YellowDark;
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Orange1;
public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
{

View File

@ -16,6 +16,7 @@ namespace osu.Game.Configuration
{
SetDefault(Static.LoginOverlayDisplayed, false);
SetDefault(Static.MutedAudioNotificationShownOnce, false);
SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
}
@ -25,6 +26,7 @@ namespace osu.Game.Configuration
{
LoginOverlayDisplayed,
MutedAudioNotificationShownOnce,
LowBatteryNotificationShownOnce,
/// <summary>
/// Info about seasonal backgrounds available fetched from API - see <see cref="APISeasonalBackgrounds"/>.

View File

@ -9,6 +9,9 @@ namespace osu.Game.Extensions
{
public static class DrawableExtensions
{
public const double REPEAT_INTERVAL = 70;
public const double INITIAL_DELAY = 250;
/// <summary>
/// Helper method that is used while <see cref="IKeyBindingHandler"/> doesn't support repetitions of <see cref="IKeyBindingHandler{T}.OnPressed"/>.
/// Simulates repetitions by continually invoking a delegate according to the default key repeat rate.
@ -19,12 +22,13 @@ namespace osu.Game.Extensions
/// <param name="handler">The <see cref="IKeyBindingHandler{T}"/> which is handling the repeat.</param>
/// <param name="scheduler">The <see cref="Scheduler"/> to schedule repetitions on.</param>
/// <param name="action">The <see cref="Action"/> to be invoked once immediately and with every repetition.</param>
/// <param name="initialRepeatDelay">The delay imposed on the first repeat. Defaults to <see cref="INITIAL_DELAY"/>.</param>
/// <returns>A <see cref="ScheduledDelegate"/> which can be cancelled to stop the repeat events from firing.</returns>
public static ScheduledDelegate BeginKeyRepeat(this IKeyBindingHandler handler, Scheduler scheduler, Action action)
public static ScheduledDelegate BeginKeyRepeat(this IKeyBindingHandler handler, Scheduler scheduler, Action action, double initialRepeatDelay = INITIAL_DELAY)
{
action();
ScheduledDelegate repeatDelegate = new ScheduledDelegate(action, handler.Time.Current + 250, 70);
ScheduledDelegate repeatDelegate = new ScheduledDelegate(action, handler.Time.Current + initialRepeatDelay, REPEAT_INTERVAL);
scheduler.Add(repeatDelegate);
return repeatDelegate;
}

View File

@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers
protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
protected override bool BlockScrollInput => false;
protected override bool BlockNonPositionalInput => true;
/// <summary>

View File

@ -23,11 +23,6 @@ namespace osu.Game.Graphics.Containers
protected const double BACKGROUND_FADE_DURATION = 800;
/// <summary>
/// Whether or not user-configured dim levels should be applied to the container.
/// </summary>
public readonly Bindable<bool> EnableUserDim = new Bindable<bool>(true);
/// <summary>
/// Whether or not user-configured settings relating to brightness of elements should be ignored
/// </summary>
@ -57,7 +52,7 @@ namespace osu.Game.Graphics.Containers
private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0;
protected float DimLevel => Math.Max(EnableUserDim.Value && !IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : 0, 0);
protected float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : 0, 0);
protected override Container<Drawable> Content => dimContent;
@ -78,7 +73,6 @@ namespace osu.Game.Graphics.Containers
LightenDuringBreaks = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks);
ShowStoryboard = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
EnableUserDim.ValueChanged += _ => UpdateVisuals();
UserDimLevel.ValueChanged += _ => UpdateVisuals();
LightenDuringBreaks.ValueChanged += _ => UpdateVisuals();
IsBreakTime.ValueChanged += _ => UpdateVisuals();

View File

@ -186,6 +186,13 @@ namespace osu.Game.Graphics
public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee");
public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff");
// in latest editor design logic, need to figure out where these sit...
public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66");
public readonly Color4 Orange1 = Color4Extensions.FromHex(@"ffd966");
// Content Background
public readonly Color4 B5 = Color4Extensions.FromHex(@"222a28");
public readonly Color4 RedLighter = Color4Extensions.FromHex(@"ffeded");
public readonly Color4 RedLight = Color4Extensions.FromHex(@"ed7787");
public readonly Color4 Red = Color4Extensions.FromHex(@"ed1121");

View File

@ -70,6 +70,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode),
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
};
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
@ -97,9 +98,7 @@ namespace osu.Game.Input.Bindings
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.IncreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume),
new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute),
@ -249,5 +248,8 @@ namespace osu.Game.Input.Bindings
[Description("Beatmap Options")]
ToggleBeatmapOptions,
[Description("Verify mode")]
EditorVerifyMode,
}
}

View File

@ -32,8 +32,6 @@ namespace osu.Game.Input.Handlers
public override bool Initialize(GameHost host) => true;
public override bool IsActive => true;
public class ReplayState<T> : IInput
where T : struct
{

View File

@ -0,0 +1,506 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using osu.Game.Database;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20210412045700_RefreshVolumeBindingsAgain")]
partial class RefreshVolumeBindingsAgain
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<float>("ApproachRate");
b.Property<float>("CircleSize");
b.Property<float>("DrainRate");
b.Property<float>("OverallDifficulty");
b.Property<double>("SliderMultiplier");
b.Property<double>("SliderTickRate");
b.HasKey("ID");
b.ToTable("BeatmapDifficulty");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("AudioLeadIn");
b.Property<double>("BPM");
b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor");
b.Property<int>("BeatmapSetInfoID");
b.Property<bool>("Countdown");
b.Property<double>("DistanceSpacing");
b.Property<int>("GridSize");
b.Property<string>("Hash");
b.Property<bool>("Hidden");
b.Property<double>("Length");
b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapID");
b.Property<string>("Path");
b.Property<int>("RulesetID");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");
b.Property<double>("StarDifficulty");
b.Property<int>("Status");
b.Property<string>("StoredBookmarks");
b.Property<double>("TimelineZoom");
b.Property<string>("Version");
b.Property<bool>("WidescreenStoryboard");
b.HasKey("ID");
b.HasIndex("BaseDifficultyID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("Hash");
b.HasIndex("MD5Hash");
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("BeatmapInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Artist");
b.Property<string>("ArtistUnicode");
b.Property<string>("AudioFile");
b.Property<string>("AuthorString")
.HasColumnName("Author");
b.Property<string>("BackgroundFile");
b.Property<int>("PreviewTime");
b.Property<string>("Source");
b.Property<string>("Tags");
b.Property<string>("Title");
b.Property<string>("TitleUnicode");
b.Property<string>("VideoFile");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.HasKey("ID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("FileInfoID");
b.ToTable("BeatmapSetFileInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset>("DateAdded");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapSetID");
b.Property<bool>("Protected");
b.Property<int>("Status");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapSetID")
.IsUnique();
b.ToTable("BeatmapSetInfo");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Key")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<int?>("SkinInfoID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("SkinInfoID");
b.HasIndex("RulesetID", "Variant");
b.ToTable("Settings");
});
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Hash");
b.Property<int>("ReferenceCount");
b.HasKey("ID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("ReferenceCount");
b.ToTable("FileInfo");
});
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntAction")
.HasColumnName("Action");
b.Property<string>("KeysString")
.HasColumnName("Keys");
b.Property<int?>("RulesetID");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("IntAction");
b.HasIndex("RulesetID", "Variant");
b.ToTable("KeyBinding");
});
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("Available");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.Property<string>("ShortName");
b.HasKey("ID");
b.HasIndex("Available");
b.HasIndex("ShortName")
.IsUnique();
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int?>("ScoreInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("ScoreInfoID");
b.ToTable("ScoreFileInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("Accuracy")
.HasColumnType("DECIMAL(1,4)");
b.Property<int>("BeatmapInfoID");
b.Property<int>("Combo");
b.Property<DateTimeOffset>("Date");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int>("MaxCombo");
b.Property<string>("ModsJson")
.HasColumnName("Mods");
b.Property<long?>("OnlineScoreID");
b.Property<double?>("PP");
b.Property<int>("Rank");
b.Property<int>("RulesetID");
b.Property<string>("StatisticsJson")
.HasColumnName("Statistics");
b.Property<long>("TotalScore");
b.Property<long?>("UserID")
.HasColumnName("UserID");
b.Property<string>("UserString")
.HasColumnName("User");
b.HasKey("ID");
b.HasIndex("BeatmapInfoID");
b.HasIndex("OnlineScoreID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("ScoreInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<string>("Name");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
.WithMany()
.HasForeignKey("BaseDifficultyID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
.WithMany("Beatmaps")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("Beatmaps")
.HasForeignKey("MetadataID");
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
.WithMany("Files")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Settings")
.HasForeignKey("SkinInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Scoring.ScoreInfo")
.WithMany("Files")
.HasForeignKey("ScoreInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
.WithMany("Scores")
.HasForeignKey("BeatmapInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
{
public partial class RefreshVolumeBindingsAgain : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DELETE FROM KeyBinding WHERE action in (6,7)");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -40,6 +40,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK.Input;
using RuntimeInfo = osu.Framework.RuntimeInfo;
@ -156,6 +157,8 @@ namespace osu.Game
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
protected virtual BatteryInfo CreateBatteryInfo() => null;
/// <summary>
/// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects.
/// </summary>
@ -281,6 +284,11 @@ namespace osu.Game
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));
var powerStatus = CreateBatteryInfo();
if (powerStatus != null)
dependencies.CacheAs(powerStatus);
dependencies.Cache(new SessionStatics());
dependencies.Cache(new OsuColour());

View File

@ -6,6 +6,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Extensions;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Volume
@ -15,8 +17,30 @@ namespace osu.Game.Overlays.Volume
public Func<GlobalAction, bool> ActionRequested;
public Func<GlobalAction, float, bool, bool> ScrollActionRequested;
public bool OnPressed(GlobalAction action) =>
ActionRequested?.Invoke(action) ?? false;
private ScheduledDelegate keyRepeat;
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.DecreaseVolume:
case GlobalAction.IncreaseVolume:
keyRepeat?.Cancel();
keyRepeat = this.BeginKeyRepeat(Scheduler, () => ActionRequested?.Invoke(action), 150);
return true;
case GlobalAction.ToggleMute:
ActionRequested?.Invoke(action);
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
keyRepeat?.Cancel();
}
protected override bool OnScroll(ScrollEvent e)
{
@ -27,9 +51,5 @@ namespace osu.Game.Overlays.Volume
public bool OnScroll(GlobalAction action, float amount, bool isPrecise) =>
ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false;
public void OnReleased(GlobalAction action)
{
}
}
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -225,7 +226,7 @@ namespace osu.Game.Overlays.Volume
private set => Bindable.Value = value;
}
private const double adjust_step = 0.05;
private const double adjust_step = 0.01;
public void Increase(double amount = 1, bool isPrecise = false) => adjust(amount, isPrecise);
public void Decrease(double amount = 1, bool isPrecise = false) => adjust(-amount, isPrecise);
@ -233,16 +234,40 @@ namespace osu.Game.Overlays.Volume
// because volume precision is set to 0.01, this local is required to keep track of more precise adjustments and only apply when possible.
private double scrollAccumulation;
private double accelerationModifier = 1;
private const double max_acceleration = 5;
private const double acceleration_multiplier = 1.8;
private ScheduledDelegate accelerationDebounce;
private void resetAcceleration() => accelerationModifier = 1;
private void adjust(double delta, bool isPrecise)
{
scrollAccumulation += delta * adjust_step * (isPrecise ? 0.1 : 1);
// every adjust increment increases the rate at which adjustments happen up to a cutoff.
// this debounce will reset on inactivity.
accelerationDebounce?.Cancel();
accelerationDebounce = Scheduler.AddDelayed(resetAcceleration, 150);
delta *= accelerationModifier;
accelerationModifier = Math.Min(max_acceleration, accelerationModifier * acceleration_multiplier);
var precision = Bindable.Precision;
while (Precision.AlmostBigger(Math.Abs(scrollAccumulation), precision))
if (isPrecise)
{
Volume += Math.Sign(scrollAccumulation) * precision;
scrollAccumulation = scrollAccumulation < 0 ? Math.Min(0, scrollAccumulation + precision) : Math.Max(0, scrollAccumulation - precision);
scrollAccumulation += delta * adjust_step * 0.1;
while (Precision.AlmostBigger(Math.Abs(scrollAccumulation), precision))
{
Volume += Math.Sign(scrollAccumulation) * precision;
scrollAccumulation = scrollAccumulation < 0 ? Math.Min(0, scrollAccumulation + precision) : Math.Max(0, scrollAccumulation - precision);
}
}
else
{
Volume += Math.Sign(delta) * Math.Max(precision, Math.Abs(delta * adjust_step));
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A ruleset-agnostic beatmap verifier that identifies issues in common metadata or mapping standards.
/// </summary>
public class BeatmapVerifier : IBeatmapVerifier
{
private readonly List<ICheck> checks = new List<ICheck>
{
new CheckBackground(),
};
public IEnumerable<Issue> Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap));
}
}

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.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckBackground : ICheck
{
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Resources, "Missing background");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateNoneSet(this),
new IssueTemplateDoesNotExist(this)
};
public IEnumerable<Issue> Run(IBeatmap beatmap)
{
if (beatmap.Metadata.BackgroundFile == null)
{
yield return new IssueTemplateNoneSet(this).Create();
yield break;
}
// If the background is set, also make sure it still exists.
var set = beatmap.BeatmapInfo.BeatmapSet;
var file = set.Files.FirstOrDefault(f => f.Filename == beatmap.Metadata.BackgroundFile);
if (file != null)
yield break;
yield return new IssueTemplateDoesNotExist(this).Create(beatmap.Metadata.BackgroundFile);
}
public class IssueTemplateNoneSet : IssueTemplate
{
public IssueTemplateNoneSet(ICheck check)
: base(check, IssueType.Problem, "No background has been set.")
{
}
public Issue Create() => new Issue(this);
}
public class IssueTemplateDoesNotExist : IssueTemplate
{
public IssueTemplateDoesNotExist(ICheck check)
: base(check, IssueType.Problem, "The background file \"{0}\" does not exist.")
{
}
public Issue Create(string filename) => new Issue(this, filename);
}
}
}

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.
namespace osu.Game.Rulesets.Edit.Checks.Components
{
/// <summary>
/// The category of an issue.
/// </summary>
public enum CheckCategory
{
/// <summary>
/// Anything to do with control points.
/// </summary>
Timing,
/// <summary>
/// Anything to do with artist, title, creator, etc.
/// </summary>
Metadata,
/// <summary>
/// Anything to do with non-audio files, e.g. background, skin, sprites, and video.
/// </summary>
Resources,
/// <summary>
/// Anything to do with audio files, e.g. song and hitsounds.
/// </summary>
Audio,
/// <summary>
/// Anything to do with files that don't fit into the above, e.g. unused, osu, or osb.
/// </summary>
Files,
/// <summary>
/// Anything to do with hitobjects unrelated to spread.
/// </summary>
Compose,
/// <summary>
/// Anything to do with difficulty levels or their progression.
/// </summary>
Spread,
/// <summary>
/// Anything to do with variables like CS, OD, AR, HP, and global SV.
/// </summary>
Settings,
/// <summary>
/// Anything to do with hitobject feedback.
/// </summary>
HitObjects,
/// <summary>
/// Anything to do with storyboarding, breaks, video offset, etc.
/// </summary>
Events
}
}

View File

@ -0,0 +1,24 @@
// 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.Rulesets.Edit.Checks.Components
{
public class CheckMetadata
{
/// <summary>
/// The category this check belongs to. E.g. <see cref="CheckCategory.Metadata"/>, <see cref="CheckCategory.Timing"/>, or <see cref="CheckCategory.Compose"/>.
/// </summary>
public readonly CheckCategory Category;
/// <summary>
/// Describes the issue(s) that this check looks for. Keep this brief, such that it fits into "No {description}". E.g. "Offscreen objects" / "Too short sliders".
/// </summary>
public readonly string Description;
public CheckMetadata(CheckCategory category, string description)
{
Category = category;
Description = description;
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Edit.Checks.Components
{
/// <summary>
/// A specific check that can be run on a beatmap to verify or find issues.
/// </summary>
public interface ICheck
{
/// <summary>
/// The metadata for this check.
/// </summary>
public CheckMetadata Metadata { get; }
/// <summary>
/// All possible templates for issues that this check may return.
/// </summary>
public IEnumerable<IssueTemplate> PossibleTemplates { get; }
/// <summary>
/// Runs this check and returns any issues detected for the provided beatmap.
/// </summary>
/// <param name="beatmap">The beatmap to run the check on.</param>
public IEnumerable<Issue> Run(IBeatmap beatmap);
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Extensions;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Edit.Checks.Components
{
public class Issue
{
/// <summary>
/// The time which this issue is associated with, if any, otherwise null.
/// </summary>
public double? Time;
/// <summary>
/// The hitobjects which this issue is associated with. Empty by default.
/// </summary>
public IReadOnlyList<HitObject> HitObjects;
/// <summary>
/// The template which this issue is using. This provides properties such as the <see cref="IssueType"/>, and the <see cref="IssueTemplate.UnformattedMessage"/>.
/// </summary>
public IssueTemplate Template;
/// <summary>
/// The check that this issue originates from.
/// </summary>
public ICheck Check => Template.Check;
/// <summary>
/// The arguments that give this issue its context, based on the <see cref="IssueTemplate"/>. These are then substituted into the <see cref="IssueTemplate.UnformattedMessage"/>.
/// This could for instance include timestamps, which diff is being compared to, what some volume is, etc.
/// </summary>
public object[] Arguments;
public Issue(IssueTemplate template, params object[] args)
{
Time = null;
HitObjects = Array.Empty<HitObject>();
Template = template;
Arguments = args;
}
public Issue(double? time, IssueTemplate template, params object[] args)
: this(template, args)
{
Time = time;
}
public Issue(HitObject hitObject, IssueTemplate template, params object[] args)
: this(template, args)
{
Time = hitObject.StartTime;
HitObjects = new[] { hitObject };
}
public Issue(IEnumerable<HitObject> hitObjects, IssueTemplate template, params object[] args)
: this(template, args)
{
var hitObjectList = hitObjects.ToList();
Time = hitObjectList.FirstOrDefault()?.StartTime;
HitObjects = hitObjectList;
}
public override string ToString() => Template.GetMessage(Arguments);
public string GetEditorTimestamp()
{
return Time == null ? string.Empty : Time.Value.ToEditorFormattedString();
}
}
}

View File

@ -0,0 +1,74 @@
// 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 Humanizer;
using osu.Framework.Graphics;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Edit.Checks.Components
{
public class IssueTemplate
{
private static readonly Color4 problem_red = new Colour4(1.0f, 0.4f, 0.4f, 1.0f);
private static readonly Color4 warning_yellow = new Colour4(1.0f, 0.8f, 0.2f, 1.0f);
private static readonly Color4 negligible_green = new Colour4(0.33f, 0.8f, 0.5f, 1.0f);
private static readonly Color4 error_gray = new Colour4(0.5f, 0.5f, 0.5f, 1.0f);
/// <summary>
/// The check that this template originates from.
/// </summary>
public readonly ICheck Check;
/// <summary>
/// The type of the issue.
/// </summary>
public readonly IssueType Type;
/// <summary>
/// The unformatted message given when this issue is detected.
/// This gets populated later when an issue is constructed with this template.
/// E.g. "Inconsistent snapping (1/{0}) with [{1}] (1/{2})."
/// </summary>
public readonly string UnformattedMessage;
public IssueTemplate(ICheck check, IssueType type, string unformattedMessage)
{
Check = check;
Type = type;
UnformattedMessage = unformattedMessage;
}
/// <summary>
/// Returns the formatted message given the arguments used to format it.
/// </summary>
/// <param name="args">The arguments used to format the message.</param>
public string GetMessage(params object[] args) => UnformattedMessage.FormatWith(args);
/// <summary>
/// Returns the colour corresponding to the type of this issue.
/// </summary>
public Colour4 Colour
{
get
{
switch (Type)
{
case IssueType.Problem:
return problem_red;
case IssueType.Warning:
return warning_yellow;
case IssueType.Negligible:
return negligible_green;
case IssueType.Error:
return error_gray;
default:
return Color4.White;
}
}
}
}
}

View File

@ -0,0 +1,25 @@
// 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.Rulesets.Edit.Checks.Components
{
/// <summary>
/// The type, or severity, of an issue.
/// </summary>
public enum IssueType
{
/// <summary> A must-fix in the vast majority of cases. </summary>
Problem,
/// <summary> A possible mistake. Often requires critical thinking. </summary>
Warning,
// TODO: Try/catch all checks run and return error templates if exceptions occur.
/// <summary> An error occurred and a complete check could not be made. </summary>
Error,
// TODO: Negligible issues should be hidden by default.
/// <summary> A possible mistake so minor/unlikely that it can often be safely ignored. </summary>
Negligible,
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A class which can run against a beatmap and surface issues to the user which could go against known criteria or hinder gameplay.
/// </summary>
public interface IBeatmapVerifier
{
public IEnumerable<Issue> Run(IBeatmap beatmap);
}
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Edit
/// <summary>
/// Whether the <see cref="HitObject"/> is currently mid-placement, but has not necessarily finished being placed.
/// </summary>
public bool PlacementActive { get; private set; }
public PlacementState PlacementActive { get; private set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
@ -72,7 +72,8 @@ namespace osu.Game.Rulesets.Edit
protected void BeginPlacement(bool commitStart = false)
{
placementHandler.BeginPlacement(HitObject);
PlacementActive |= commitStart;
if (commitStart)
PlacementActive = PlacementState.Active;
}
/// <summary>
@ -82,10 +83,19 @@ namespace osu.Game.Rulesets.Edit
/// <param name="commit">Whether the object should be committed.</param>
public void EndPlacement(bool commit)
{
if (!PlacementActive)
BeginPlacement();
switch (PlacementActive)
{
case PlacementState.Finished:
return;
case PlacementState.Waiting:
// ensure placement was started before ending to make state handling simpler.
BeginPlacement();
break;
}
placementHandler.EndPlacement(HitObject, commit);
PlacementActive = false;
PlacementActive = PlacementState.Finished;
}
/// <summary>
@ -94,7 +104,7 @@ namespace osu.Game.Rulesets.Edit
/// <param name="result">The snap result information.</param>
public virtual void UpdateTimeAndPosition(SnapResult result)
{
if (!PlacementActive)
if (PlacementActive == PlacementState.Waiting)
HitObject.StartTime = result.Time ?? EditorClock?.CurrentTime ?? Time.Current;
}
@ -125,5 +135,12 @@ namespace osu.Game.Rulesets.Edit
return false;
}
}
public enum PlacementState
{
Waiting,
Active,
Finished
}
}
}

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Configuration;
@ -49,7 +50,7 @@ namespace osu.Game.Rulesets.Mods
/// The user readable description of this mod.
/// </summary>
[JsonIgnore]
public virtual string Description => string.Empty;
public abstract string Description { get; }
/// <summary>
/// The tooltip to display for this mod when used in a <see cref="ModIcon"/>.
@ -170,7 +171,12 @@ namespace osu.Game.Rulesets.Mods
target.UnbindFrom(sourceBindable);
}
else
target.Parse(source);
{
if (!(target is IParseable parseable))
throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}.");
parseable.Parse(source);
}
}
public bool Equals(IMod other) => other is Mod them && Equals(them);

View File

@ -37,8 +37,7 @@ namespace osu.Game.Rulesets.Mods
public void ApplyToPlayer(Player player)
{
player.ApplyToBackground(b => b.EnableUserDim.Value = false);
player.ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
player.BreakOverlay.Hide();

View File

@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Mods
{
public override string Name => "No Mod";
public override string Acronym => "NM";
public override string Description => "No mods applied.";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Ban;
public override ModType Type => ModType.System;

View File

@ -56,8 +56,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
public virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;
private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<DrawableHitObject>();
private readonly List<DrawableHitObject> nestedHitObjects = new List<DrawableHitObject>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects;
/// <summary>
/// Whether this object should handle any user input events.
@ -249,7 +249,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
// Must be done before the nested DHO is added to occur before the nested Apply()!
drawableNested.ParentHitObject = this;
nestedHitObjects.Value.Add(drawableNested);
nestedHitObjects.Add(drawableNested);
AddNestedHitObject(drawableNested);
}
@ -305,19 +305,16 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (Samples != null)
Samples.Samples = null;
if (nestedHitObjects.IsValueCreated)
foreach (var obj in nestedHitObjects)
{
foreach (var obj in nestedHitObjects.Value)
{
obj.OnNewResult -= onNewResult;
obj.OnRevertResult -= onRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
nestedHitObjects.Value.Clear();
ClearNestedHitObjects();
obj.OnNewResult -= onNewResult;
obj.OnRevertResult -= onRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
nestedHitObjects.Clear();
ClearNestedHitObjects();
HitObject.DefaultsApplied -= onDefaultsApplied;
OnFree();

View File

@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Objects
/// </summary>
public readonly Bindable<double?> ExpectedDistance = new Bindable<double?>();
public bool HasValidLength => Distance > 0;
/// <summary>
/// The control points of the path.
/// </summary>

View File

@ -1,9 +1,10 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
@ -17,80 +18,101 @@ namespace osu.Game.Rulesets.Replays
public abstract class FramedReplayInputHandler<TFrame> : ReplayInputHandler
where TFrame : ReplayFrame
{
private readonly Replay replay;
/// <summary>
/// Whether we have at least one replay frame.
/// </summary>
public bool HasFrames => Frames.Count != 0;
protected List<ReplayFrame> Frames => replay.Frames;
/// <summary>
/// Whether we are waiting for new frames to be received.
/// </summary>
public bool WaitingForFrame => !replay.HasReceivedAllFrames && currentFrameIndex == Frames.Count - 1;
public TFrame CurrentFrame
{
get
{
if (!HasFrames || !currentFrameIndex.HasValue)
return null;
/// <summary>
/// The current frame of the replay.
/// The current time is always between the start and the end time of the current frame.
/// </summary>
/// <remarks>Returns null if the current time is strictly before the first frame.</remarks>
public TFrame? CurrentFrame => currentFrameIndex == -1 ? null : (TFrame)Frames[currentFrameIndex];
return (TFrame)Frames[currentFrameIndex.Value];
}
}
/// <summary>
/// The next frame of the replay.
/// The start time of <see cref="NextFrame"/> is always greater or equal to the start time of <see cref="CurrentFrame"/> regardless of the seeking direction.
/// </summary>
/// <remarks>Returns null if the current frame is the last frame.</remarks>
public TFrame? NextFrame => currentFrameIndex == Frames.Count - 1 ? null : (TFrame)Frames[currentFrameIndex + 1];
public TFrame NextFrame
/// <summary>
/// The frame for the start value of the interpolation of the replay movement.
/// </summary>
/// <exception cref="InvalidOperationException">The replay is empty.</exception>
public TFrame StartFrame
{
get
{
if (!HasFrames)
return null;
throw new InvalidOperationException($"Attempted to get {nameof(StartFrame)} of an empty replay");
if (!currentFrameIndex.HasValue)
return currentDirection > 0 ? (TFrame)Frames[0] : null;
int nextFrame = clampedNextFrameIndex;
if (nextFrame == currentFrameIndex.Value)
return null;
return (TFrame)Frames[clampedNextFrameIndex];
return (TFrame)Frames[Math.Max(0, currentFrameIndex)];
}
}
private int? currentFrameIndex;
private int clampedNextFrameIndex =>
currentFrameIndex.HasValue ? Math.Clamp(currentFrameIndex.Value + currentDirection, 0, Frames.Count - 1) : 0;
protected FramedReplayInputHandler(Replay replay)
/// <summary>
/// The frame for the end value of the interpolation of the replay movement.
/// </summary>
/// <exception cref="InvalidOperationException">The replay is empty.</exception>
public TFrame EndFrame
{
this.replay = replay;
get
{
if (!HasFrames)
throw new InvalidOperationException($"Attempted to get {nameof(EndFrame)} of an empty replay");
return (TFrame)Frames[Math.Min(currentFrameIndex + 1, Frames.Count - 1)];
}
}
private const double sixty_frame_time = 1000.0 / 60;
protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;
protected double? CurrentTime { get; private set; }
private int currentDirection = 1;
/// <summary>
/// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data.
/// Disabling this can make replay playback smoother (useful for autoplay, currently).
/// </summary>
public bool FrameAccuratePlayback;
public bool HasFrames => Frames.Count > 0;
// This input handler should be enabled only if there is at least one replay frame.
public override bool IsActive => HasFrames;
protected double CurrentTime { get; private set; }
protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;
protected List<ReplayFrame> Frames => replay.Frames;
private readonly Replay replay;
private int currentFrameIndex;
private const double sixty_frame_time = 1000.0 / 60;
protected FramedReplayInputHandler(Replay replay)
{
// TODO: This replay frame ordering should be enforced on the Replay type.
// Currently, the ordering can be broken if the frames are added after this construction.
replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time));
this.replay = replay;
currentFrameIndex = -1;
CurrentTime = double.NegativeInfinity;
}
private bool inImportantSection
{
get
{
if (!HasFrames || !FrameAccuratePlayback)
if (!HasFrames || !FrameAccuratePlayback || currentFrameIndex == -1)
return false;
var frame = currentDirection > 0 ? CurrentFrame : NextFrame;
if (frame == null)
return false;
return IsImportant(frame) && // a button is in a pressed state
Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
return IsImportant(StartFrame) && // a button is in a pressed state
Math.Abs(CurrentTime - EndFrame.Time) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
}
}
@ -105,71 +127,52 @@ namespace osu.Game.Rulesets.Replays
/// <returns>The usable time value. If null, we should not advance time as we do not have enough data.</returns>
public override double? SetFrameFromTime(double time)
{
updateDirection(time);
Debug.Assert(currentDirection != 0);
if (!HasFrames)
{
// in the case all frames are received, allow time to progress regardless.
// In the case all frames are received, allow time to progress regardless.
if (replay.HasReceivedAllFrames)
return CurrentTime = time;
return null;
}
TFrame next = NextFrame;
double frameStart = getFrameTime(currentFrameIndex);
double frameEnd = getFrameTime(currentFrameIndex + 1);
// if we have a next frame, check if it is before or at the current time in playback, and advance time to it if so.
if (next != null)
// If the proposed time is after the current frame end time, we progress forwards to precisely the new frame's time (regardless of incoming time).
if (frameEnd <= time)
{
int compare = time.CompareTo(next.Time);
if (compare == 0 || compare == currentDirection)
{
currentFrameIndex = clampedNextFrameIndex;
return CurrentTime = CurrentFrame.Time;
}
time = frameEnd;
currentFrameIndex++;
}
// If the proposed time is before the current frame start time, and we are at the frame boundary, we progress backwards.
else if (time < frameStart && CurrentTime == frameStart)
currentFrameIndex--;
// at this point, the frame index can't be advanced.
// even so, we may be able to propose the clock progresses forward due to being at an extent of the replay,
// or moving towards the next valid frame (ie. interpolating in a non-important section).
frameStart = getFrameTime(currentFrameIndex);
frameEnd = getFrameTime(currentFrameIndex + 1);
// the exception is if currently in an important section, which is respected above all.
if (inImportantSection)
// Pause until more frames are arrived.
if (WaitingForFrame && frameStart < time)
{
Debug.Assert(next != null || !replay.HasReceivedAllFrames);
CurrentTime = frameStart;
return null;
}
// if a next frame does exist, allow interpolation.
if (next != null)
return CurrentTime = time;
CurrentTime = Math.Clamp(time, frameStart, frameEnd);
// if all frames have been received, allow playing beyond extents.
if (replay.HasReceivedAllFrames)
return CurrentTime = time;
// if not all frames are received but we are before the first frame, allow playing.
if (time < Frames[0].Time)
return CurrentTime = time;
// in the case we have no next frames and haven't received enough frame data, block.
return null;
// In an important section, a mid-frame time cannot be used and a null is returned instead.
return inImportantSection && frameStart < time && time < frameEnd ? null : (double?)CurrentTime;
}
private void updateDirection(double time)
private double getFrameTime(int index)
{
if (!CurrentTime.HasValue)
{
currentDirection = 1;
}
else
{
currentDirection = time.CompareTo(CurrentTime);
if (currentDirection == 0) currentDirection = 1;
}
if (index < 0)
return double.NegativeInfinity;
if (index >= Frames.Count)
return double.PositiveInfinity;
return Frames[index].Time;
}
}
}

View File

@ -201,6 +201,8 @@ namespace osu.Game.Rulesets
public virtual HitObjectComposer CreateHitObjectComposer() => null;
public virtual IBeatmapVerifier CreateBeatmapVerifier() => null;
public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle };
public virtual IResourceStore<byte[]> CreateResourceStore() => new NamespacedResourceStore<byte[]>(new DllResourceStore(GetType().Assembly), @"Resources");

View File

@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public int JudgedHits { get; private set; }
private JudgementResult lastAppliedResult;
private readonly BindableBool hasCompleted = new BindableBool();
/// <summary>
@ -53,12 +55,11 @@ namespace osu.Game.Rulesets.Scoring
public void ApplyResult(JudgementResult result)
{
JudgedHits++;
lastAppliedResult = result;
ApplyResultInternal(result);
NewJudgement?.Invoke(result);
updateHasCompleted();
}
/// <summary>
@ -69,8 +70,6 @@ namespace osu.Game.Rulesets.Scoring
{
JudgedHits--;
updateHasCompleted();
RevertResultInternal(result);
}
@ -134,6 +133,10 @@ namespace osu.Game.Rulesets.Scoring
}
}
private void updateHasCompleted() => hasCompleted.Value = JudgedHits == MaxHits;
protected override void Update()
{
base.Update();
hasCompleted.Value = JudgedHits == MaxHits && (JudgedHits == 0 || lastAppliedResult.TimeAbsolute < Clock.CurrentTime);
}
}
}

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.UI
var enumerable = HitObjectContainer.Objects;
if (nestedPlayfields.IsValueCreated)
if (nestedPlayfields.Count != 0)
enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects));
return enumerable;
@ -76,9 +76,9 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>.
/// </summary>
public IEnumerable<Playfield> NestedPlayfields => nestedPlayfields.IsValueCreated ? nestedPlayfields.Value : Enumerable.Empty<Playfield>();
public IEnumerable<Playfield> NestedPlayfields => nestedPlayfields;
private readonly Lazy<List<Playfield>> nestedPlayfields = new Lazy<List<Playfield>>();
private readonly List<Playfield> nestedPlayfields = new List<Playfield>();
/// <summary>
/// Whether judgements should be displayed by this and and all nested <see cref="Playfield"/>s.
@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.UI
otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h);
otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
nestedPlayfields.Value.Add(otherPlayfield);
nestedPlayfields.Add(otherPlayfield);
}
protected override void LoadComplete()
@ -279,12 +279,7 @@ namespace osu.Game.Rulesets.UI
return true;
}
bool removedFromNested = false;
if (nestedPlayfields.IsValueCreated)
removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(hitObject));
return removedFromNested;
return nestedPlayfields.Any(p => p.Remove(hitObject));
}
/// <summary>
@ -429,10 +424,7 @@ namespace osu.Game.Rulesets.UI
return;
}
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var p in nestedPlayfields.Value)
foreach (var p in nestedPlayfields)
p.SetKeepAlive(hitObject, keepAlive);
}
@ -444,10 +436,7 @@ namespace osu.Game.Rulesets.UI
foreach (var (_, entry) in lifetimeEntryMap)
entry.KeepAlive = true;
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var p in nestedPlayfields.Value)
foreach (var p in nestedPlayfields)
p.KeepAllAlive();
}
@ -461,10 +450,7 @@ namespace osu.Game.Rulesets.UI
{
HitObjectContainer.PastLifetimeExtension = value;
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var nested in nestedPlayfields.Value)
foreach (var nested in nestedPlayfields)
nested.PastLifetimeExtension = value;
}
}
@ -479,10 +465,7 @@ namespace osu.Game.Rulesets.UI
{
HitObjectContainer.FutureLifetimeExtension = value;
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var nested in nestedPlayfields.Value)
foreach (var nested in nestedPlayfields)
nested.FutureLifetimeExtension = value;
}
}

View File

@ -27,9 +27,12 @@ namespace osu.Game.Screens.Backgrounds
private WorkingBeatmap beatmap;
/// <summary>
/// Whether or not user dim settings should be applied to this Background.
/// Whether or not user-configured settings relating to brightness of elements should be ignored.
/// </summary>
public readonly Bindable<bool> EnableUserDim = new Bindable<bool>();
/// <remarks>
/// Beatmap background screens should not apply user settings by default.
/// </remarks>
public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>(true);
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
@ -50,7 +53,7 @@ namespace osu.Game.Screens.Backgrounds
InternalChild = dimmable = CreateFadeContainer();
dimmable.EnableUserDim.BindTo(EnableUserDim);
dimmable.IgnoreUserSettings.BindTo(IgnoreUserSettings);
dimmable.IsBreakTime.BindTo(IsBreakTime);
dimmable.BlurAmount.BindTo(BlurAmount);
@ -148,7 +151,7 @@ namespace osu.Game.Screens.Backgrounds
/// <summary>
/// As an optimisation, we add the two blur portions to be applied rather than actually applying two separate blurs.
/// </summary>
private Vector2 blurTarget => EnableUserDim.Value
private Vector2 blurTarget => !IgnoreUserSettings.Value
? new Vector2(BlurAmount.Value + (float)userBlurLevel.Value * USER_BLUR_FACTOR)
: new Vector2(BlurAmount.Value);
@ -166,7 +169,9 @@ namespace osu.Game.Screens.Backgrounds
BlurAmount.ValueChanged += _ => UpdateVisuals();
}
protected override bool ShowDimContent => !ShowStoryboard.Value || !StoryboardReplacesBackground.Value; // The background needs to be hidden in the case of it being replaced by the storyboard
protected override bool ShowDimContent
// The background needs to be hidden in the case of it being replaced by the storyboard
=> (!ShowStoryboard.Value && !IgnoreUserSettings.Value) || !StoryboardReplacesBackground.Value;
protected override void UpdateVisuals()
{

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
}
[BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.Yellow;
private void load(OsuColour colours) => Colour = colours.GreyCarmineLight;
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
public class ControlPointVisualisation : PointVisualisation
{
protected readonly ControlPoint Point;
public ControlPointVisualisation(ControlPoint point)
{
Point = point;
Height = 0.25f;
Origin = Anchor.TopCentre;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = Point.GetRepresentingColour(colours);
}
}
}

View File

@ -0,0 +1,72 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
public class EffectPointVisualisation : CompositeDrawable
{
private readonly EffectControlPoint effect;
private Bindable<bool> kiai;
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private OsuColour colours { get; set; }
public EffectPointVisualisation(EffectControlPoint point)
{
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Y;
effect = point;
}
[BackgroundDependencyLoader]
private void load()
{
kiai = effect.KiaiModeBindable.GetBoundCopy();
kiai.BindValueChanged(_ =>
{
ClearInternal();
AddInternal(new ControlPointVisualisation(effect));
if (!kiai.Value)
return;
var endControlPoint = beatmap.ControlPointInfo.EffectPoints.FirstOrDefault(c => c.Time > effect.Time && !c.KiaiMode);
// handle kiai duration
// eventually this will be simpler when we have control points with durations.
if (endControlPoint != null)
{
RelativeSizeAxes = Axes.Both;
Origin = Anchor.TopLeft;
Width = (float)(endControlPoint.Time - effect.Time);
AddInternal(new PointVisualisation
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.TopLeft,
Width = 1,
Height = 0.25f,
Depth = float.MaxValue,
Colour = effect.GetRepresentingColour(colours).Darken(0.5f),
});
}
}, true);
}
}
}

View File

@ -1,29 +1,33 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
public class GroupVisualisation : PointVisualisation
public class GroupVisualisation : CompositeDrawable
{
[Resolved]
private OsuColour colours { get; set; }
public readonly ControlPointGroup Group;
private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
[Resolved]
private OsuColour colours { get; set; }
public GroupVisualisation(ControlPointGroup group)
: base(group.Time)
{
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both;
Origin = Anchor.TopLeft;
Group = group;
X = (float)group.Time;
}
protected override void LoadComplete()
@ -33,13 +37,32 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
controlPoints.BindTo(Group.ControlPoints);
controlPoints.BindCollectionChanged((_, __) =>
{
if (controlPoints.Count == 0)
{
Colour = Color4.Transparent;
return;
}
ClearInternal();
Colour = controlPoints.Any(c => c is TimingControlPoint) ? colours.YellowDark : colours.Green;
if (controlPoints.Count == 0)
return;
foreach (var point in Group.ControlPoints)
{
switch (point)
{
case TimingControlPoint _:
AddInternal(new ControlPointVisualisation(point) { Y = 0, });
break;
case DifficultyControlPoint _:
AddInternal(new ControlPointVisualisation(point) { Y = 0.25f, });
break;
case SampleControlPoint _:
AddInternal(new ControlPointVisualisation(point) { Y = 0.5f, });
break;
case EffectControlPoint effect:
AddInternal(new EffectPointVisualisation(effect) { Y = 0.75f });
break;
}
}
}, true);
}
}

View File

@ -27,6 +27,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Y = -10,
Height = 0.35f
},
new BookmarkPart
@ -38,6 +39,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
},
new Container
{
Name = "centre line",
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray5,
Children = new Drawable[]
@ -45,7 +47,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
new Circle
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Origin = Anchor.Centre,
Size = new Vector2(5)
},
new Box
@ -59,7 +61,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
new Circle
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
Origin = Anchor.Centre,
Size = new Vector2(5)
},
}
@ -69,7 +71,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Height = 0.25f
Height = 0.10f
}
};
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
@ -10,19 +9,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
/// <summary>
/// Represents a spanning point on a timeline part.
/// </summary>
public class DurationVisualisation : Container
public class DurationVisualisation : Circle
{
protected DurationVisualisation(double startTime, double endTime)
{
Masking = true;
CornerRadius = 5;
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both;
X = (float)startTime;
Width = (float)(endTime - startTime);
AddInternal(new Box { RelativeSizeAxes = Axes.Both });
}
}
}

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