1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-23 19:07:20 +08:00

Merge branch 'master' into osu-random-mod-improvements

This commit is contained in:
Dean Herbert 2021-07-13 19:31:37 +09:00 committed by GitHub
commit db8ffc6316
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
297 changed files with 6375 additions and 2955 deletions

View File

@ -27,7 +27,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.608.0",
"version": "2021.705.0",
"commands": [
"localisation"
]

View File

@ -157,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning
#Style - variable declaration
csharp_style_inlined_variable_declaration = true:warning
csharp_style_deconstructed_variable_declaration = true:warning
csharp_style_deconstructed_variable_declaration = false:silent
#Style - other C# 7.x features
dotnet_style_prefer_inferred_tuple_names = true:warning
@ -168,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
#Style - C# 8 features
csharp_prefer_static_local_function = true:warning
csharp_prefer_simple_using_statement = true:silent
csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_range_operator = true:warning
csharp_style_prefer_index_operator = false:silent
csharp_style_prefer_range_operator = false:silent
csharp_style_prefer_switch_expression = false:none
#Supressing roslyn built-in analyzers

View File

@ -11,7 +11,7 @@
A free-to-win rhythm game. Rhythm is just a *click* away!
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew.
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
## Status
@ -23,7 +23,7 @@ We are accepting bug reports (please report with as much detail as possible and
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
- Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where lazer is currently and the roadmap going forward.
- Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where the project is currently and the roadmap going forward.
## Running osu!

View File

@ -12,7 +12,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>

View File

@ -12,7 +12,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>

View File

@ -12,7 +12,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>

View File

@ -12,7 +12,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@ -68,6 +68,8 @@ namespace osu.Desktop.Updater
return false;
}
scheduleRecheck = false;
if (notification == null)
{
notification = new UpdateProgressNotification(this) { State = ProgressNotificationState.Active };
@ -98,7 +100,6 @@ namespace osu.Desktop.Updater
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
scheduleRecheck = false;
}
else
{
@ -110,13 +111,14 @@ namespace osu.Desktop.Updater
catch (Exception)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
}
finally
{
if (scheduleRecheck)
{
// check again in 30 minutes.
Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
@ -141,7 +143,7 @@ namespace osu.Desktop.Updater
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
.ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit()));
return true;
};
}

View File

@ -5,8 +5,8 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
<AssemblyName>osu!</AssemblyName>
<Title>osu!lazer</Title>
<Product>osu!lazer</Product>
<Title>osu!</Title>
<Product>osu!</Product>
<ApplicationIcon>lazer.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.0.0</Version>

View File

@ -3,7 +3,7 @@
<metadata>
<id>osulazer</id>
<version>0.0.0</version>
<title>osu!lazer</title>
<title>osu!</title>
<authors>ppy Pty Ltd</authors>
<owners>Dean Herbert</owners>
<projectUrl>https://osu.ppy.sh/</projectUrl>
@ -20,4 +20,3 @@
<file src="**.config" target="lib\net45\"/>
</files>
</package>

View File

@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class CatchEditorTestSceneContainer : Container
{
[Cached(typeof(Playfield))]
public readonly ScrollingPlayfield Playfield;
protected override Container<Drawable> Content { get; }
public CatchEditorTestSceneContainer()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Width = CatchPlayfield.WIDTH;
Height = 1000;
Padding = new MarginPadding
{
Bottom = 100
};
InternalChildren = new Drawable[]
{
new ScrollingTestContainer(ScrollingDirection.Down)
{
TimeRange = 1000,
RelativeSizeAxes = Axes.Both,
Child = Playfield = new TestCatchPlayfield
{
RelativeSizeAxes = Axes.Both
}
},
new PlayfieldBorder
{
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Full },
Clock = new FramedClock(new StopwatchClock(true))
},
Content = new Container
{
RelativeSizeAxes = Axes.Both
}
};
}
private class TestCatchPlayfield : CatchEditorPlayfield
{
public TestCatchPlayfield()
: base(new BeatmapDifficulty { CircleSize = 0 })
{
}
}
}
}

View File

@ -0,0 +1,79 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public abstract class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
{
protected const double TIME_SNAP = 100;
protected DrawableCatchHitObject LastObject;
protected new ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
protected override Container<Drawable> Content => contentContainer;
private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchPlacementBlueprintTestScene()
{
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
contentContainer.Playfield.Clock = new FramedClock(new ManualClock());
}
[SetUp]
public void Setup() => Schedule(() =>
{
HitObjectContainer.Clear();
ResetPlacement();
LastObject = null;
});
protected void AddMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
{
float y = HitObjectContainer.PositionAtTime(time);
Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
InputManager.MoveMouseTo(pos);
});
protected void AddClickStep(MouseButton button) => AddStep($"click {button}", () =>
{
InputManager.Click(button);
});
protected IEnumerable<FruitOutline> FruitOutlines => Content.ChildrenOfType<FruitOutline>();
// Unused because AddHitObject is overriden
protected override Container CreateHitObjectContainer() => new Container();
protected override void AddHitObject(DrawableHitObject hitObject)
{
LastObject = (DrawableCatchHitObject)hitObject;
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
}
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
{
var result = base.SnapForBlueprint(blueprint);
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
return result;
}
}
}

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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
protected ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
protected override Container<Drawable> Content => contentContainer;
private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchSelectionBlueprintTestScene()
{
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
}
}
}

View File

@ -0,0 +1,87 @@
// 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.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneBananaShowerPlacementBlueprint : CatchPlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
protected override void AddHitObject(DrawableHitObject hitObject)
{
// Create nested bananas (but positions are not randomized because beatmap processing is not done).
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
base.AddHitObject(hitObject);
}
[Test]
public void TestBasicPlacement()
{
const double start_time = 100;
const double end_time = 500;
AddMoveStep(start_time, 0);
AddClickStep(MouseButton.Left);
AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Right);
AddAssert("banana shower is placed", () => LastObject is DrawableBananaShower);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
}
[Test]
public void TestReversePlacement()
{
const double start_time = 100;
const double end_time = 500;
AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Left);
AddMoveStep(start_time, 0);
AddClickStep(MouseButton.Right);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
}
[Test]
public void TestFinishWithZeroDuration()
{
AddMoveStep(100, 0);
AddClickStep(MouseButton.Left);
AddClickStep(MouseButton.Right);
AddAssert("banana shower is not placed", () => LastObject == null);
AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting);
}
[Test]
public void TestOpacity()
{
AddMoveStep(100, 0);
AddClickStep(MouseButton.Left);
AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
AddMoveStep(200, 0);
AddUntilStep("outline is opaque", () => Precision.AlmostEquals(timeSpanOutline.Alpha, 1));
AddMoveStep(100, 0);
AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
}
private TimeSpanOutline timeSpanOutline => Content.ChildrenOfType<TimeSpanOutline>().Single();
}
}

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 System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneFruitPlacementBlueprint : CatchPlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
[Test]
public void TestFruitPlacementPosition()
{
const double time = 300;
const float x = CatchPlayfield.CENTER_X;
AddMoveStep(time, x);
AddClickStep(MouseButton.Left);
AddAssert("outline position is correct", () =>
{
var outline = FruitOutlines.Single();
return Precision.AlmostEquals(outline.X, x) &&
Precision.AlmostEquals(outline.Y, HitObjectContainer.PositionAtTime(time));
});
AddAssert("fruit time is correct", () => Precision.AlmostEquals(LastObject.StartTimeBindable.Value, time));
AddAssert("fruit position is correct", () => Precision.AlmostEquals(LastObject.X, x));
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
{
public TestSceneJuiceStreamSelectionBlueprint()
{
var hitObject = new JuiceStream
{
OriginalX = 100,
StartTime = 100,
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(200, 100),
new Vector2(0, 200),
}),
};
var controlPoint = new ControlPointInfo();
controlPoint.Add(0, new TimingControlPoint
{
BeatLength = 100
});
hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 });
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
}
}
}

View File

@ -0,0 +1,114 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using Direction = osu.Game.Rulesets.Catch.UI.Direction;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneCatchSkinConfiguration : OsuTestScene
{
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
private Catcher catcher;
private readonly Container container;
public TestSceneCatchSkinConfiguration()
{
Add(droppedObjectContainer = new DroppedObjectContainer());
Add(container = new Container { RelativeSizeAxes = Axes.Both });
}
[TestCase(false)]
[TestCase(true)]
public void TestCatcherPlateFlipping(bool flip)
{
AddStep("setup catcher", () =>
{
var skin = new TestSkin { FlipCatcherPlate = flip };
container.Child = new SkinProvidingContainer(skin)
{
Child = catcher = new Catcher(new Container())
{
Anchor = Anchor.Centre
}
};
});
Fruit fruit = new Fruit();
AddStep("catch fruit", () => catchFruit(fruit, 20));
float position = 0;
AddStep("record fruit position", () => position = getCaughtObjectPosition(fruit));
AddStep("face left", () => catcher.VisualDirection = Direction.Left);
if (flip)
AddAssert("fruit position changed", () => !Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
else
AddAssert("fruit position unchanged", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
AddStep("face right", () => catcher.VisualDirection = Direction.Right);
AddAssert("fruit position restored", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
}
private float getCaughtObjectPosition(Fruit fruit)
{
var caughtObject = catcher.ChildrenOfType<CaughtObject>().Single(c => c.HitObject == fruit);
return caughtObject.Parent.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
}
private void catchFruit(Fruit fruit, float x)
{
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableFruit = new DrawableFruit(fruit) { X = x };
var judgement = fruit.CreateJudgement();
catcher.OnNewResult(drawableFruit, new CatchJudgementResult(fruit, judgement)
{
Type = judgement.MaxResult
});
}
private class TestSkin : DefaultSkin
{
public bool FlipCatcherPlate { get; set; }
public TestSkin()
: base(null)
{
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is CatchSkinConfiguration config)
{
if (config == CatchSkinConfiguration.FlipCatcherPlate)
return SkinUtils.As<TValue>(new Bindable<bool>(FlipCatcherPlate));
}
return base.GetConfig<TLookup, TValue>(lookup);
}
}
}
}

View File

@ -194,9 +194,9 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
checkPlate(10);
AddAssert("caught objects are stacked", () =>
catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
catcher.CaughtObjects.Any(obj => obj.Y < -25));
catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
catcher.CaughtObjects.Any(obj => obj.Y < 0));
}
[Test]

View File

@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("finish hyper-dashing", () =>
{
catcherArea.MovableCatcher.SetHyperDashState(1);
catcherArea.MovableCatcher.SetHyperDashState();
catcherArea.MovableCatcher.FinishTransforms();
});

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -6,6 +6,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
@ -23,5 +24,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
: base(new THitObject())
{
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
}
}

View File

@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
public abstract class CatchSelectionBlueprint<THitObject> : HitObjectSelectionBlueprint<THitObject>
where THitObject : CatchHitObject
{
protected override bool AlwaysShowWhenSelected => true;
public override Vector2 ScreenSpaceSelectionPoint
{
get

View File

@ -1,6 +1,7 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -18,7 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
Anchor = Anchor.BottomLeft;
Origin = Anchor.Centre;
Size = new Vector2(2 * CatchHitObject.OBJECT_RADIUS);
InternalChild = new BorderPiece();
}
@ -28,10 +28,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
Colour = osuColour.Yellow;
}
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject, [CanBeNull] CatchHitObject parent = null)
{
X = hitObject.EffectiveX;
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
X = hitObject.EffectiveX - (parent?.OriginalX ?? 0);
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime, parent?.StartTime ?? hitObjectContainer.Time.Current);
Scale = new Vector2(hitObject.Scale);
}
}

View File

@ -0,0 +1,53 @@
// 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.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class NestedOutlineContainer : CompositeDrawable
{
private readonly List<CatchHitObject> nestedHitObjects = new List<CatchHitObject>();
public NestedOutlineContainer()
{
Anchor = Anchor.BottomLeft;
}
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
{
X = parentHitObject.OriginalX;
Y = hitObjectContainer.PositionAtTime(parentHitObject.StartTime);
}
public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
{
nestedHitObjects.Clear();
nestedHitObjects.AddRange(parentHitObject.NestedHitObjects
.OfType<CatchHitObject>()
.Where(h => !(h is TinyDroplet)));
while (nestedHitObjects.Count < InternalChildren.Count)
RemoveInternal(InternalChildren[^1]);
while (InternalChildren.Count < nestedHitObjects.Count)
AddInternal(new FruitOutline());
for (int i = 0; i < nestedHitObjects.Count; i++)
{
var hitObject = nestedHitObjects[i];
var outline = (FruitOutline)InternalChildren[i];
outline.UpdateFrom(hitObjectContainer, hitObject, parentHitObject);
outline.Scale *= hitObject is Droplet ? 0.5f : 1;
}
}
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
}

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class ScrollingPath : CompositeDrawable
{
private readonly Path drawablePath;
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
public ScrollingPath()
{
Anchor = Anchor.BottomLeft;
InternalChildren = new Drawable[]
{
drawablePath = new SmoothPath
{
PathRadius = 2,
Alpha = 0.5f
},
};
}
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
{
X = hitObject.OriginalX;
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
}
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
computeDistanceXs(hitObject);
drawablePath.Vertices = vertices
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
.ToArray();
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
}
private void computeDistanceXs(JuiceStream hitObject)
{
vertices.Clear();
var sliderVertices = new List<Vector2>();
hitObject.Path.GetPathToProgress(sliderVertices, 0, 1);
if (sliderVertices.Count == 0)
return;
double distance = 0;
Vector2 lastPosition = Vector2.Zero;
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
{
foreach (var position in sliderVertices)
{
distance += Vector2.Distance(lastPosition, position);
lastPosition = position;
vertices.Add((distance, position.X));
}
sliderVertices.Reverse();
}
}
// Because this has 0x0 size, the contents are otherwise masked away if the start position is outside the screen.
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
}

View File

@ -3,7 +3,10 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osuTK;
@ -17,9 +20,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private float minNestedX;
private float maxNestedX;
private readonly ScrollingPath scrollingPath;
private readonly NestedOutlineContainer nestedOutlineContainer;
private readonly Cached pathCache = new Cached();
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
: base(hitObject)
{
InternalChildren = new Drawable[]
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer()
};
}
[BackgroundDependencyLoader]
@ -29,7 +43,28 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
computeObjectBounds();
}
private void onDefaultsApplied(HitObject _) => computeObjectBounds();
protected override void Update()
{
base.Update();
if (!IsSelected) return;
scrollingPath.UpdatePositionFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdatePositionFrom(HitObjectContainer, HitObject);
if (pathCache.IsValid) return;
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
pathCache.Validate();
}
private void onDefaultsApplied(HitObject _)
{
computeObjectBounds();
pathCache.Invalidate();
}
private void computeObjectBounds()
{

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
@ -20,6 +22,16 @@ namespace osu.Game.Rulesets.Catch.Edit
{
}
[BackgroundDependencyLoader]
private void load()
{
LayerBelowRuleset.Add(new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
});
}
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods);

View File

@ -1,9 +1,12 @@
// 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.Framework.Allocation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@ -24,23 +27,90 @@ namespace osu.Game.Rulesets.Catch.Edit
var blueprint = moveEvent.Blueprint;
Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint);
Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
float deltaX = targetPosition.X - originalPosition.X;
deltaX = limitMovement(deltaX, EditorBeatmap.SelectedHitObjects);
if (deltaX == 0)
{
// Even if there is no positional change, there may be a time change.
return true;
}
EditorBeatmap.PerformOnSelection(h =>
{
if (!(h is CatchHitObject hitObject)) return;
if (hitObject is BananaShower) return;
// TODO: confine in bounds
hitObject.OriginalXBindable.Value += deltaX;
hitObject.OriginalX += deltaX;
// Move the nested hit objects to give an instant result before nested objects are recreated.
foreach (var nested in hitObject.NestedHitObjects.OfType<CatchHitObject>())
nested.OriginalXBindable.Value += deltaX;
nested.OriginalX += deltaX;
});
return true;
}
/// <summary>
/// Limit positional movement of the objects by the constraint that moved objects should stay in bounds.
/// </summary>
/// <param name="deltaX">The positional movement.</param>
/// <param name="movingObjects">The objects to be moved.</param>
/// <returns>The positional movement with the restriction applied.</returns>
private float limitMovement(float deltaX, IEnumerable<HitObject> movingObjects)
{
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
foreach (float x in movingObjects.SelectMany(getOriginalPositions))
{
minX = Math.Min(minX, x);
maxX = Math.Max(maxX, x);
}
// To make an object with position `x` stay in bounds after `deltaX` movement, `0 <= x + deltaX <= WIDTH` should be satisfied.
// Subtracting `x`, we get `-x <= deltaX <= WIDTH - x`.
// We only need to apply the inequality to extreme values of `x`.
float lowerBound = -minX;
float upperBound = CatchPlayfield.WIDTH - maxX;
// The inequality may be unsatisfiable if the objects were already out of bounds.
// In that case, don't move objects at all.
if (lowerBound > upperBound)
return 0;
return Math.Clamp(deltaX, lowerBound, upperBound);
}
/// <summary>
/// Enumerate X positions that should be contained in-bounds after move offset is applied.
/// </summary>
private IEnumerable<float> getOriginalPositions(HitObject hitObject)
{
switch (hitObject)
{
case Fruit fruit:
yield return fruit.OriginalX;
break;
case JuiceStream juiceStream:
foreach (var nested in juiceStream.NestedHitObjects.OfType<CatchHitObject>())
{
// Even if `OriginalX` is outside the playfield, tiny droplets can be moved inside the playfield after the random offset application.
if (!(nested is TinyDroplet))
yield return nested.OriginalX;
}
break;
case BananaShower _:
// A banana shower occupies the whole screen width.
// If the selection contains a banana shower, the selection cannot be moved horizontally.
yield return 0;
yield return CatchPlayfield.WIDTH;
break;
}
}
}
}

View File

@ -12,37 +12,29 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.CircleSize,
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 1,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
public BindableBool HardRockOffsets { get; } = new BindableBool();
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
CircleSize.MaxValue = extended ? 11 : 10;
ApproachRate.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get
@ -61,20 +53,12 @@ namespace osu.Game.Rulesets.Catch.Mods
}
}
protected override void TransferSettings(BeatmapDifficulty difficulty)
{
base.TransferSettings(difficulty);
TransferSetting(CircleSize, difficulty.CircleSize);
TransferSetting(ApproachRate, difficulty.ApproachRate);
}
protected override void ApplySettings(BeatmapDifficulty difficulty)
{
base.ApplySettings(difficulty);
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
}
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)

View File

@ -1,6 +1,7 @@
// 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 Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -20,6 +21,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <summary>
/// The horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary>
/// <remarks>
/// Only setter is exposed.
/// Use <see cref="OriginalX"/> or <see cref="EffectiveX"/> to get the horizontal position.
/// </remarks>
[JsonIgnore]
public float X
{
set => OriginalXBindable.Value = value;
@ -34,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public float XOffset
{
get => XOffsetBindable.Value;
set => XOffsetBindable.Value = value;
}
@ -44,7 +51,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original <see cref="X"/> value specified in the beatmap, not affected by the beatmap processing.
/// Use <see cref="EffectiveX"/> for a gameplay.
/// </remarks>
public float OriginalX => OriginalXBindable.Value;
public float OriginalX
{
get => OriginalXBindable.Value;
set => OriginalXBindable.Value = value;
}
/// <summary>
/// The effective horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>.
@ -53,9 +64,9 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original <see cref="X"/> value plus the offset applied by the beatmap processing.
/// Use <see cref="OriginalX"/> if a value not affected by the offset is desired.
/// </remarks>
public float EffectiveX => OriginalXBindable.Value + XOffsetBindable.Value;
public float EffectiveX => OriginalX + XOffset;
public double TimePreempt = 1000;
public double TimePreempt { get; set; } = 1000;
public readonly Bindable<int> IndexInBeatmapBindable = new Bindable<int>();

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -25,7 +26,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; }
[JsonIgnore]
public double Velocity { get; private set; }
[JsonIgnore]
public double TickDistance { get; private set; }
/// <summary>
@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public float EndX => OriginalX + this.CurvePositionAt(1).X;
[JsonIgnore]
public double Duration
{
get => this.SpanCount() * Path.Distance / Velocity;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
@ -33,6 +34,7 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <summary>
/// The target fruit if we are to initiate a hyperdash.
/// </summary>
[JsonIgnore]
public CatchHitObject HyperDashTarget
{
get => hyperDashTarget;

View File

@ -0,0 +1,13 @@
// 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.Catch.Skinning
{
public enum CatchSkinConfiguration
{
/// <summary>
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
/// </summary>
FlipCatcherPlate
}
}

View File

@ -103,6 +103,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
return (IBindable<TValue>)result;
case CatchSkinConfiguration config:
switch (config)
{
case CatchSkinConfiguration.FlipCatcherPlate:
// Don't flip catcher plate contents if the catcher is provided by this legacy skin.
if (GetDrawableComponent(new CatchSkinComponent(CatchSkinComponents.Catcher)) != null)
return (IBindable<TValue>)new Bindable<bool>();
break;
}
break;
}
return base.GetConfig<TLookup, TValue>(lookup);

View File

@ -56,11 +56,6 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
/// <summary>
/// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
/// </summary>
public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5;
/// <summary>
/// The amount by which caught fruit should be scaled down to fit on the plate.
/// </summary>
@ -84,8 +79,8 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherAnimationState CurrentState
{
get => body.AnimationState.Value;
private set => body.AnimationState.Value = value;
get => Body.AnimationState.Value;
private set => Body.AnimationState.Value = value;
}
/// <summary>
@ -108,18 +103,22 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
public Direction VisualDirection
{
get => Scale.X > 0 ? Direction.Right : Direction.Left;
set => Scale = new Vector2((value == Direction.Right ? 1 : -1) * Math.Abs(Scale.X), Scale.Y);
}
/// <summary>
/// The currently facing direction.
/// </summary>
public Direction VisualDirection { get; set; } = Direction.Right;
/// <summary>
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
/// </summary>
private bool flipCatcherPlate;
/// <summary>
/// Width of the area that can be used to attempt catches during gameplay.
/// </summary>
private readonly float catchWidth;
private readonly SkinnableCatcher body;
internal readonly SkinnableCatcher Body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
@ -157,8 +156,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
// offset fruit vertically to better place "above" the plate.
Y = -5
},
body = new SkinnableCatcher(),
Body = new SkinnableCatcher(),
hitExplosionContainer = new HitExplosionContainer
{
Anchor = Anchor.TopCentre,
@ -347,6 +348,8 @@ namespace osu.Game.Rulesets.Catch.UI
trails.HyperDashTrailsColour = hyperDashColour;
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
flipCatcherPlate = skin.GetConfig<CatchSkinConfiguration, bool>(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
runHyperDashStateTransition(HyperDashing);
}
@ -354,6 +357,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
Body.Scale = scaleFromDirection;
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
(hyperDashDirection < 0 && hyperDashTargetPosition > X))
@ -388,9 +395,6 @@ namespace osu.Game.Rulesets.Catch.UI
float adjustedRadius = displayRadius * lenience_adjust;
float checkDistance = MathF.Pow(adjustedRadius, 2);
// offset fruit vertically to better place "above" the plate.
position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET;
while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance))
{
position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius);
@ -465,7 +469,7 @@ namespace osu.Game.Rulesets.Catch.UI
break;
case DroppedObjectAnimation.Explode:
var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * Scale.X;
float originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * caughtObjectContainer.Scale.X;
d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.MoveToX(d.X + originalX * 6, 1000);
d.FadeOut(750);

View File

@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.UI
CatcherTrail sprite = trailPool.Get();
sprite.AnimationState = catcher.CurrentState;
sprite.Scale = catcher.Scale;
sprite.Scale = catcher.Scale * catcher.Body.Scale;
sprite.Position = catcher.Position;
target.Add(sprite);

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -0,0 +1,145 @@
// 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.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
{
[TestFixture]
public class CheckTooShortSlidersTest
{
private CheckTooShortSliders check;
[SetUp]
public void Setup()
{
check = new CheckTooShortSliders();
}
[Test]
public void TestLongSlider()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(100, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { slider });
}
[Test]
public void TestShortSlider()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(25, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { slider });
}
[Test]
public void TestTooShortSliderExpert()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(10, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { slider }, DifficultyRating.Expert);
}
[Test]
public void TestTooShortSlider()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(10, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertTooShort(new List<HitObject> { slider });
}
[Test]
public void TestTooShortSliderWithRepeats()
{
// Would be ok if we looked at the duration, but not if we look at the span duration.
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 2,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(10, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertTooShort(new List<HitObject> { slider });
}
private void assertOk(List<HitObject> hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy)
{
Assert.That(check.Run(getContext(hitObjects, difficultyRating)), Is.Empty);
}
private void assertTooShort(List<HitObject> hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy)
{
var issues = check.Run(getContext(hitObjects, difficultyRating)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckTooShortSliders.IssueTemplateTooShort);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, DifficultyRating difficultyRating)
{
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
}
}
}

View File

@ -0,0 +1,116 @@
// 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.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
{
[TestFixture]
public class CheckTooShortSpinnersTest
{
private CheckTooShortSpinners check;
private BeatmapDifficulty difficulty;
[SetUp]
public void Setup()
{
check = new CheckTooShortSpinners();
difficulty = new BeatmapDifficulty();
}
[Test]
public void TestLongSpinner()
{
Spinner spinner = new Spinner { StartTime = 0, Duration = 4000 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertOk(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestShortSpinner()
{
Spinner spinner = new Spinner { StartTime = 0, Duration = 750 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertOk(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestVeryShortSpinner()
{
// Spinners at a certain duration only get 1000 points if approached by auto at a certain angle, making it difficult to determine.
Spinner spinner = new Spinner { StartTime = 0, Duration = 475 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertVeryShort(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestTooShortSpinner()
{
Spinner spinner = new Spinner { StartTime = 0, Duration = 400 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertTooShort(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestTooShortSpinnerVaryingOd()
{
const double duration = 450;
var difficultyLowOd = new BeatmapDifficulty { OverallDifficulty = 1 };
Spinner spinnerLowOd = new Spinner { StartTime = 0, Duration = duration };
spinnerLowOd.ApplyDefaults(new ControlPointInfo(), difficultyLowOd);
var difficultyHighOd = new BeatmapDifficulty { OverallDifficulty = 10 };
Spinner spinnerHighOd = new Spinner { StartTime = 0, Duration = duration };
spinnerHighOd.ApplyDefaults(new ControlPointInfo(), difficultyHighOd);
assertOk(new List<HitObject> { spinnerLowOd }, difficultyLowOd);
assertTooShort(new List<HitObject> { spinnerHighOd }, difficultyHighOd);
}
private void assertOk(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty);
}
private void assertVeryShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort);
}
private void assertTooShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = hitObjects,
BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty }
};
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
case OsuSkinConfiguration osuLookup:
if (osuLookup == OsuSkinConfiguration.CursorCentre)
return SkinUtils.As<TValue>(new BindableBool(false));
return SkinUtils.As<TValue>(new BindableBool());
break;
}

View File

@ -37,11 +37,13 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly BindableBool snakingIn = new BindableBool();
private readonly BindableBool snakingOut = new BindableBool();
private IBeatmap beatmap;
private const double duration_of_span = 3605;
private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
=> new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache)
@ -51,8 +53,16 @@ namespace osu.Game.Rulesets.Osu.Tests
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
}
private Slider slider;
private DrawableSlider drawableSlider;
[SetUp]
public void Setup() => Schedule(() =>
{
slider = null;
drawableSlider = null;
});
[SetUpSteps]
public override void SetUpSteps()
{
@ -67,21 +77,19 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
addSeekStep(startTime);
retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
retrieveSlider(sliderIndex);
setSnaking(true);
ensureSnakingIn(startTime + fade_in_modifier);
addEnsureSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i < sliderIndex; i++)
{
// non-final repeats should not snake out
ensureNoSnakingOut(startTime, i);
addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
// final repeat should snake out
ensureSnakingOut(startTime, sliderIndex);
addEnsureSnakingOutSteps(() => slider.StartTime, sliderIndex);
}
[TestCase(0)]
@ -93,17 +101,15 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
addSeekStep(startTime);
retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
retrieveSlider(sliderIndex);
setSnaking(false);
ensureNoSnakingIn(startTime + fade_in_modifier);
addEnsureNoSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i <= sliderIndex; i++)
{
// no snaking out ever, including final repeat
ensureNoSnakingOut(startTime, i);
addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
}
@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// repeat might have a chance to update its position depending on where in the frame its hit,
// so some leniency is allowed here instead of checking strict equality
checkPositionChange(16600, sliderRepeat, positionAlmostSame);
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
}
[Test]
@ -126,38 +132,41 @@ namespace osu.Game.Rulesets.Osu.Tests
setSnaking(true);
base.SetUpSteps();
checkPositionChange(16600, sliderRepeat, positionDecreased);
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
}
private void retrieveDrawableSlider(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased);
private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame);
private void ensureSnakingOut(double startTime, int repeatIndex)
private void retrieveSlider(int index)
{
var repeatTime = timeAtRepeat(startTime, repeatIndex);
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
addSeekStep(() => slider);
AddUntilStep("retrieve drawable slider", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
}
private void addEnsureSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased);
private void addEnsureNoSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionRemainsSame);
private void addEnsureSnakingOutSteps(Func<double> startTime, int repeatIndex)
{
if (repeatIndex % 2 == 0)
checkPositionChange(repeatTime, sliderStart, positionIncreased);
addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderStart, positionIncreased);
else
checkPositionChange(repeatTime, sliderEnd, positionDecreased);
addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderEnd, positionDecreased);
}
private void ensureNoSnakingOut(double startTime, int repeatIndex) =>
checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
private void addEnsureNoSnakingOutStep(Func<double> startTime, int repeatIndex)
=> addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)sliderStart : sliderEnd;
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)getSliderStart : getSliderEnd;
private List<Vector2> sliderCurve => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private Vector2 sliderStart() => sliderCurve.First();
private Vector2 sliderEnd() => sliderCurve.Last();
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private Vector2 getSliderStart() => getSliderCurve().First();
private Vector2 getSliderEnd() => getSliderCurve().Last();
private Vector2 sliderRepeat()
private Vector2 getSliderRepeat()
{
var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]);
var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == beatmap.HitObjects[1]);
var repeat = drawable.ChildrenOfType<Container<DrawableSliderRepeat>>().First().Children.First();
return repeat.Position;
}
@ -167,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y;
private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1);
private void checkPositionChange(double startTime, Func<Vector2> positionToCheck, Func<Vector2, Vector2, bool> positionAssertion)
private void addCheckPositionChangeSteps(Func<double> startTime, Func<Vector2> positionToCheck, Func<Vector2, Vector2, bool> positionAssertion)
{
Vector2 previousPosition = Vector2.Zero;
@ -176,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(startTime);
AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke());
addSeekStep(startTime + 100);
addSeekStep(() => startTime() + 100);
AddAssert($"{positionDescription} {assertionDescription}", () =>
{
var currentPosition = positionToCheck.Invoke();
@ -193,19 +202,21 @@ namespace osu.Game.Rulesets.Osu.Tests
});
}
private void addSeekStep(double time)
private void addSeekStep(Func<Slider> slider)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
private void addSeekStep(Func<double> time)
{
HitObjects = hitObjects
};
AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time()));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
private readonly List<HitObject> hitObjects = new List<HitObject>
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() };
private static List<HitObject> createHitObjects() => new List<HitObject>
{
new Slider
{

View File

@ -5,7 +5,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -0,0 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckTooShortSliders : ICheck
{
/// <summary>
/// The shortest acceptable duration between the head and tail of the slider (so ignoring repeats).
/// </summary>
private const double span_duration_threshold = 125; // 240 BPM 1/2
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short sliders");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooShort(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
if (context.InterpretedDifficulty > DifficultyRating.Easy)
yield break;
foreach (var hitObject in context.Beatmap.HitObjects)
{
if (hitObject is Slider slider && slider.SpanDuration < span_duration_threshold)
yield return new IssueTemplateTooShort(this).Create(slider);
}
}
public class IssueTemplateTooShort : IssueTemplate
{
public IssueTemplateTooShort(ICheck check)
: base(check, IssueType.Problem, "This slider is too short ({0:0} ms), expected at least {1:0} ms.")
{
}
public Issue Create(Slider slider) => new Issue(slider, this, slider.SpanDuration, span_duration_threshold);
}
}
}

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 osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckTooShortSpinners : ICheck
{
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short spinners");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooShort(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
// These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner.
// It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners.
double warningThreshold = 500 + (od < 5 ? (5 - od) * -21.8 : (od - 5) * 20); // Anything above this is always ok.
double problemThreshold = 450 + (od < 5 ? (5 - od) * -17 : (od - 5) * 17); // Anything below this is never ok.
foreach (var hitObject in context.Beatmap.HitObjects)
{
if (!(hitObject is Spinner spinner))
continue;
if (spinner.Duration < problemThreshold)
yield return new IssueTemplateTooShort(this).Create(spinner);
else if (spinner.Duration < warningThreshold)
yield return new IssueTemplateVeryShort(this).Create(spinner);
}
}
public class IssueTemplateTooShort : IssueTemplate
{
public IssueTemplateTooShort(ICheck check)
: base(check, IssueType.Problem, "This spinner is too short. Auto cannot achieve 1000 points on this.")
{
}
public Issue Create(Spinner spinner) => new Issue(spinner, this);
}
public class IssueTemplateVeryShort : IssueTemplate
{
public IssueTemplateVeryShort(ICheck check)
: base(check, IssueType.Warning, "This spinner may be too short. Ensure auto can achieve 1000 points on this.")
{
}
public Issue Create(Spinner spinner) => new Issue(spinner, this);
}
}
}

View File

@ -15,10 +15,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
// Compose
new CheckOffscreenObjects(),
new CheckTooShortSpinners(),
// Spread
new CheckTimeDistanceEquality(),
new CheckLowDiffOverlaps()
new CheckLowDiffOverlaps(),
new CheckTooShortSliders(),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)

View File

@ -0,0 +1,16 @@
// 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.Osu.Mods
{
/// <summary>
/// Marker interface for any mod which completely hides the approach circles.
/// Used for incompatibility with <see cref="IRequiresApproachCircles"/>.
/// </summary>
/// <remarks>
/// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour.
/// </remarks>
public interface IHidesApproachCircles
{
}
}

View File

@ -1,12 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Osu.Mods
{
/// <summary>
/// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes.
/// </summary>
public interface IMutateApproachCircles
{
}
}

View File

@ -0,0 +1,16 @@
// 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.Osu.Mods
{
/// <summary>
/// Marker interface for any mod which requires the approach circles to be visible.
/// Used for incompatibility with <see cref="IHidesApproachCircles"/>.
/// </summary>
/// <remarks>
/// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour.
/// </remarks>
public interface IRequiresApproachCircles
{
}
}

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IRequiresApproachCircles
{
public override string Name => "Approach Different";
public override string Acronym => "AD";
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4)

View File

@ -158,17 +158,17 @@ namespace osu.Game.Rulesets.Osu.Mods
var firstObj = beatmap.HitObjects[0];
var startDelay = firstObj.StartTime - firstObj.TimePreempt;
using (BeginAbsoluteSequence(startDelay + break_close_late, true))
using (BeginAbsoluteSequence(startDelay + break_close_late))
leaveBreak();
foreach (var breakInfo in beatmap.Breaks)
{
if (breakInfo.HasEffect)
{
using (BeginAbsoluteSequence(breakInfo.StartTime - break_open_early, true))
using (BeginAbsoluteSequence(breakInfo.StartTime - break_open_early))
{
enterBreak();
using (BeginDelayedSequence(breakInfo.Duration + break_open_early + break_close_late, true))
using (BeginDelayedSequence(breakInfo.Duration + break_open_early + break_close_late))
leaveBreak();
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@ -11,34 +10,26 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.CircleSize,
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
CircleSize.MaxValue = extended ? 11 : 10;
ApproachRate.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get
@ -55,20 +46,12 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
protected override void TransferSettings(BeatmapDifficulty difficulty)
{
base.TransferSettings(difficulty);
TransferSetting(CircleSize, difficulty.CircleSize);
TransferSetting(ApproachRate, difficulty.ApproachRate);
}
protected override void ApplySettings(BeatmapDifficulty difficulty)
{
base.ApplySettings(difficulty);
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
}
}
}

View File

@ -15,12 +15,12 @@ using osu.Game.Rulesets.Osu.Skinning;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHidden : ModHidden, IMutateApproachCircles
public class OsuModHidden : ModHidden, IHidesApproachCircles
{
public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
/// <summary>
/// Adjusts the size of hit objects during their fade in animation.
/// </summary>
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IHidesApproachCircles
{
public override ModType Type => ModType.Fun;
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{

View File

@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles
public class OsuModSpinIn : ModWithVisibilityAdjustment, IHidesApproachCircles
{
public override string Name => "Spin In";
public override string Acronym => "SI";
@ -21,8 +21,9 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Circles spin in. No approach circles.";
public override double ScoreMultiplier => 1;
// todo: this mod should be able to be compatible with hidden with a bit of further implementation.
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
// further implementation will be required for supporting that.
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) };
private const int rotate_offset = 360;
private const float rotate_starting_width = 2;
@ -43,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
case DrawableHitCircle circle:
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
{
circle.ApproachCircle.Hide();

View File

@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles
public class OsuModTraceable : ModWithVisibilityAdjustment, IRequiresApproachCircles
{
public override string Name => "Traceable";
public override string Acronym => "TC";
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void applyCirclePieceState(DrawableOsuHitObject hitObject, IDrawable hitCircle = null)
{
var h = hitObject.HitObject;
using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
(hitCircle ?? hitObject).Hide();
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1;
double moveDuration = hitObject.TimePreempt + 1;
using (drawable.BeginAbsoluteSequence(appearTime, true))
using (drawable.BeginAbsoluteSequence(appearTime))
{
drawable
.MoveToOffset(appearOffset)

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
for (int i = 0; i < amountWiggles; i++)
{
using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration, true))
using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration))
wiggle();
}
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Mods
for (int i = 0; i < amountWiggles; i++)
{
using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration, true))
using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration))
wiggle();
}
}

View File

@ -233,35 +233,43 @@ namespace osu.Game.Rulesets.Osu.Replays
// Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt));
bool hasWaited = false;
if (waitTime > lastFrame.Time)
{
lastFrame = new OsuReplayFrame(waitTime, lastFrame.Position) { Actions = lastFrame.Actions };
hasWaited = true;
AddFrameToReplay(lastFrame);
}
Vector2 lastPosition = lastFrame.Position;
double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
OsuReplayFrame lastLastFrame = Frames.Count >= 2 ? (OsuReplayFrame)Frames[^2] : null;
// Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
if (timeDifference > 0 && // Sanity checks
((lastPosition - targetPos).Length > h.Radius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough
timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
if (timeDifference > 0)
{
// Perform eased movement
// If the last frame is a key-up frame and there has been no wait period, adjust the last frame's position such that it begins eased movement instantaneously.
if (lastLastFrame != null && lastFrame is OsuKeyUpReplayFrame && !hasWaited)
{
// [lastLastFrame] ... [lastFrame] ... [current frame]
// We want to find the cursor position at lastFrame, so interpolate between lastLastFrame and the new target position.
lastFrame.Position = Interpolation.ValueAt(lastFrame.Time, lastFrame.Position, targetPos, lastLastFrame.Time, h.StartTime, easing);
}
Vector2 lastPosition = lastFrame.Position;
// Perform the rest of the eased movement until the target position is reached.
for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time))
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing);
AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions });
}
}
buttonIndex = 0;
}
else
{
// Start alternating once the time separation is too small (faster than ~225BPM).
if (timeDifference > 0 && timeDifference < 266)
buttonIndex++;
}
else
buttonIndex = 0;
}
/// <summary>
@ -284,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Replays
// TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime.
double hEndTime = h.GetEndTime() + KEY_UP_DELAY;
int endDelay = h is Spinner ? 1 : 0;
var endFrame = new OsuReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y));
var endFrame = new OsuKeyUpReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y));
// Decrement because we want the previous frame, not the next one
int index = FindInsertionIndex(startFrame) - 1;
@ -381,5 +389,13 @@ namespace osu.Game.Rulesets.Osu.Replays
}
#endregion
private class OsuKeyUpReplayFrame : OsuReplayFrame
{
public OsuKeyUpReplayFrame(double time, Vector2 position)
: base(time, position)
{
}
}
}
}

View File

@ -130,18 +130,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Spinner spinner = drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
this.ScaleTo(initial_scale);
this.RotateTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true))
using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
{
switch (state)
{
@ -157,17 +157,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
}
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
centre.ScaleTo(0);
mainContainer.ScaleTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
updateComplete(state == ArmedState.Hit, 0);
}

View File

@ -86,6 +86,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public override void ApplyTransformsAt(double time, bool propagateChildren = false)
{
// For the same reasons as above w.r.t rewinding, we shouldn't propagate to children here either.
// ReSharper disable once RedundantArgumentDefaultValue - removing the "redundant" default value triggers BaseMethodCallWithDefaultParameter
base.ApplyTransformsAt(time, false);
}

View File

@ -100,17 +100,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case DrawableSpinner d:
Spinner spinner = d.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
fixedMiddle.FadeColour(Color4.White);
using (BeginDelayedSequence(spinner.TimePreempt, true))
using (BeginDelayedSequence(spinner.TimePreempt))
fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
}

View File

@ -89,10 +89,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Spinner spinner = d.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
/// <remarks>
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
/// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
/// This offset is negated to bring all constants into window-space.
/// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
/// </remarks>
protected const float SPINNER_TOP_OFFSET = 45f - 16f;
@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
double startTime = Math.Min(Time.Current, DrawableSpinner.HitStateUpdateTime - 400);
using (BeginAbsoluteSequence(startTime, true))
using (BeginAbsoluteSequence(startTime))
{
clear.FadeInFromZero(400, Easing.Out);
@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
const double fade_out_duration = 50;
using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration, true))
using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration))
clear.FadeOut(fade_out_duration);
}
else
@ -182,14 +182,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength))
spin.FadeOutFromOne(spinFadeOutLength);
break;
case DrawableSpinnerTick d:
if (state == ArmedState.Hit)
{
using (BeginAbsoluteSequence(d.HitStateUpdateTime, true))
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
spin.FadeOut(300);
}

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@ -11,14 +10,13 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ScrollSpeed { get; } = new BindableFloat
[SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable ScrollSpeed { get; } = new DifficultyBindable
{
Precision = 0.05f,
MinValue = 0.25f,
MaxValue = 4,
Default = 1,
Value = 1,
ReadCurrentFromDifficulty = _ => 1,
};
public override string SettingDescription
@ -39,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
base.ApplySettings(difficulty);
ApplySetting(ScrollSpeed, scroll => difficulty.SliderMultiplier *= scroll);
if (ScrollSpeed.Value != null) difficulty.SliderMultiplier *= ScrollSpeed.Value.Value;
}
}
}

View File

@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.UpdateStartTimeStateTransforms();
using (BeginDelayedSequence(-ring_appear_offset, true))
using (BeginDelayedSequence(-ring_appear_offset))
targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint);
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestSingleSpan()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestRepeat()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestNonEvenTicks()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestLegacyLastTickOffset()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray();
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick));
Assert.That(events[2].Time, Is.EqualTo(900));
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps
const double velocity = 5;
const double min_distance = velocity * 10;
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray();
Assert.Multiple(() =>
{

View File

@ -38,19 +38,28 @@ namespace osu.Game.Tests.Database
[Test]
public void TestDefaultsPopulationAndQuery()
{
Assert.That(query().Count, Is.EqualTo(0));
Assert.That(queryCount(), Is.EqualTo(0));
KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer);
Assert.That(query().Count, Is.EqualTo(3));
Assert.That(queryCount(), Is.EqualTo(3));
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1));
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2));
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1));
Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2));
}
private IQueryable<RealmKeyBinding> query() => realmContextFactory.Context.All<RealmKeyBinding>();
private int queryCount(GlobalAction? match = null)
{
using (var usage = realmContextFactory.GetForRead())
{
var results = usage.Realm.All<RealmKeyBinding>();
if (match.HasValue)
results = results.Where(k => k.ActionInt == (int)match.Value);
return results.Count();
}
}
[Test]
public void TestUpdateViaQueriedReference()
@ -59,25 +68,28 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer);
var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
var tsr = ThreadSafeReference.Create(backBinding);
using (var usage = realmContextFactory.GetForWrite())
using (var primaryUsage = realmContextFactory.GetForRead())
{
var binding = usage.Realm.ResolveReference(tsr);
binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
var backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
usage.Commit();
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
var tsr = ThreadSafeReference.Create(backBinding);
using (var usage = realmContextFactory.GetForWrite())
{
var binding = usage.Realm.ResolveReference(tsr);
binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
usage.Commit();
}
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
// check still correct after re-query.
backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
// check still correct after re-query.
backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
[TearDown]

View File

@ -0,0 +1,94 @@
// 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 Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckZeroLengthObjectsTest
{
private CheckZeroLengthObjects check;
[SetUp]
public void Setup()
{
check = new CheckZeroLengthObjects();
}
[Test]
public void TestCircle()
{
assertOk(new List<HitObject>
{
new HitCircle { StartTime = 1000, Position = new Vector2(0, 0) }
});
}
[Test]
public void TestRegularSlider()
{
assertOk(new List<HitObject>
{
getSliderMock(1000).Object
});
}
[Test]
public void TestZeroLengthSlider()
{
assertZeroLength(new List<HitObject>
{
getSliderMock(0).Object
});
}
[Test]
public void TestNegativeLengthSlider()
{
assertZeroLength(new List<HitObject>
{
getSliderMock(-1000).Object
});
}
private Mock<Slider> getSliderMock(double duration)
{
var mockSlider = new Mock<Slider>();
mockSlider.As<IHasDuration>().Setup(d => d.Duration).Returns(duration);
return mockSlider;
}
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assertZeroLength(List<HitObject> hitObjects)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckZeroLengthObjects.IssueTemplateZeroLength);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,41 @@
// 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;
namespace osu.Game.Tests.Localisation
{
[TestFixture]
public class BeatmapMetadataRomanisationTest
{
[Test]
public void TestRomanisation()
{
var metadata = new BeatmapMetadata
{
Artist = "Romanised Artist",
ArtistUnicode = "Unicode Artist",
Title = "Romanised title",
TitleUnicode = "Unicode Title"
};
var romanisableString = metadata.ToRomanisableString();
Assert.AreEqual(metadata.ToString(), romanisableString.Romanised);
Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original);
}
[Test]
public void TestRomanisationNoUnicode()
{
var metadata = new BeatmapMetadata
{
Artist = "Romanised Artist",
Title = "Romanised title"
};
var romanisableString = metadata.ToRomanisableString();
Assert.AreEqual(romanisableString.Romanised, romanisableString.Original);
}
}
}

View File

@ -0,0 +1,165 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Mods
{
[TestFixture]
public class ModDifficultyAdjustTest
{
private TestModDifficultyAdjust testMod;
[SetUp]
public void Setup()
{
testMod = new TestModDifficultyAdjust();
}
[Test]
public void TestUnchangedSettingsFollowAppliedDifficulty()
{
var result = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 10,
OverallDifficulty = 10
});
Assert.That(result.DrainRate, Is.EqualTo(10));
Assert.That(result.OverallDifficulty, Is.EqualTo(10));
result = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 1,
OverallDifficulty = 1
});
Assert.That(result.DrainRate, Is.EqualTo(1));
Assert.That(result.OverallDifficulty, Is.EqualTo(1));
}
[Test]
public void TestChangedSettingsOverrideAppliedDifficulty()
{
testMod.OverallDifficulty.Value = 4;
var result = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 10,
OverallDifficulty = 10
});
Assert.That(result.DrainRate, Is.EqualTo(10));
Assert.That(result.OverallDifficulty, Is.EqualTo(4));
result = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 1,
OverallDifficulty = 1
});
Assert.That(result.DrainRate, Is.EqualTo(1));
Assert.That(result.OverallDifficulty, Is.EqualTo(4));
}
[Test]
public void TestChangedSettingsRetainedWhenSameValueIsApplied()
{
testMod.OverallDifficulty.Value = 4;
// Apply and de-apply the same value as the mod.
applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 4 });
var result = applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 10 });
Assert.That(result.OverallDifficulty, Is.EqualTo(4));
}
[Test]
public void TestChangedSettingSerialisedWhenSameValueIsApplied()
{
applyDifficulty(new BeatmapDifficulty { OverallDifficulty = 4 });
testMod.OverallDifficulty.Value = 4;
var result = (TestModDifficultyAdjust)new APIMod(testMod).ToMod(new TestRuleset());
Assert.That(result.OverallDifficulty.Value, Is.EqualTo(4));
}
[Test]
public void TestChangedSettingsRevertedToDefault()
{
applyDifficulty(new BeatmapDifficulty
{
DrainRate = 10,
OverallDifficulty = 10
});
testMod.OverallDifficulty.Value = 4;
testMod.ResetSettingsToDefaults();
Assert.That(testMod.DrainRate.Value, Is.Null);
Assert.That(testMod.OverallDifficulty.Value, Is.Null);
var applied = applyDifficulty(new BeatmapDifficulty
{
DrainRate = 10,
OverallDifficulty = 10
});
Assert.That(applied.OverallDifficulty, Is.EqualTo(10));
}
/// <summary>
/// Applies a <see cref="BeatmapDifficulty"/> to the mod and returns a new <see cref="BeatmapDifficulty"/>
/// representing the result if the mod were applied to a fresh <see cref="BeatmapDifficulty"/> instance.
/// </summary>
private BeatmapDifficulty applyDifficulty(BeatmapDifficulty difficulty)
{
// ensure that ReadFromDifficulty doesn't pollute the values.
var newDifficulty = difficulty.Clone();
testMod.ReadFromDifficulty(difficulty);
testMod.ApplyToDifficulty(newDifficulty);
return newDifficulty;
}
private class TestModDifficultyAdjust : ModDifficultyAdjust
{
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type)
{
if (type == ModType.DifficultyIncrease)
yield return new TestModDifficultyAdjust();
}
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
{
throw new System.NotImplementedException();
}
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap)
{
throw new System.NotImplementedException();
}
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap)
{
throw new System.NotImplementedException();
}
public override string Description => string.Empty;
public override string ShortName => string.Empty;
}
}
}

View File

@ -184,6 +184,9 @@ namespace osu.Game.Tests.NonVisual
Assert.DoesNotThrow(() => osu.Migrate(customPath2));
Assert.That(File.Exists(Path.Combine(customPath2, database_filename)));
// some files may have been left behind for whatever reason, but that's not what we're testing here.
customPath = prepareCustomPath();
Assert.DoesNotThrow(() => osu.Migrate(customPath));
Assert.That(File.Exists(Path.Combine(customPath, database_filename)));
}

View File

@ -0,0 +1,123 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
namespace osu.Game.Tests.NonVisual
{
public class FirstAvailableHitWindowsTest
{
private TestDrawableRuleset testDrawableRuleset;
[SetUp]
public void Setup()
{
testDrawableRuleset = new TestDrawableRuleset();
}
[Test]
public void TestResultIfOnlyParentHitWindowIsEmpty()
{
var testObject = new TestHitObject(HitWindows.Empty);
HitObject nested = new TestHitObject(new HitWindows());
testObject.AddNested(nested);
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, nested.HitWindows);
}
[Test]
public void TestResultIfParentHitWindowsIsNotEmpty()
{
var testObject = new TestHitObject(new HitWindows());
HitObject nested = new TestHitObject(new HitWindows());
testObject.AddNested(nested);
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, testObject.HitWindows);
}
[Test]
public void TestResultIfParentAndChildHitWindowsAreEmpty()
{
var firstObject = new TestHitObject(HitWindows.Empty);
HitObject nested = new TestHitObject(HitWindows.Empty);
firstObject.AddNested(nested);
var secondObject = new TestHitObject(new HitWindows());
testDrawableRuleset.HitObjects = new List<HitObject> { firstObject, secondObject };
Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, secondObject.HitWindows);
}
[Test]
public void TestResultIfAllHitWindowsAreEmpty()
{
var firstObject = new TestHitObject(HitWindows.Empty);
HitObject nested = new TestHitObject(HitWindows.Empty);
firstObject.AddNested(nested);
testDrawableRuleset.HitObjects = new List<HitObject> { firstObject };
Assert.IsNull(testDrawableRuleset.FirstAvailableHitWindows);
}
[SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")]
private class TestDrawableRuleset : DrawableRuleset
{
public List<HitObject> HitObjects;
public override IEnumerable<HitObject> Objects => HitObjects;
public override event Action<JudgementResult> NewResult;
public override event Action<JudgementResult> RevertResult;
public override Playfield Playfield { get; }
public override Container Overlays { get; }
public override Container FrameStableComponents { get; }
public override IFrameStableClock FrameStableClock { get; }
internal override bool FrameStablePlayback { get; set; }
public override IReadOnlyList<Mod> Mods { get; }
public override double GameplayStartTime { get; }
public override GameplayCursorContainer Cursor { get; }
public TestDrawableRuleset()
: base(new OsuRuleset())
{
// won't compile without this.
NewResult?.Invoke(null);
RevertResult?.Invoke(null);
}
public override void SetReplayScore(Score replayScore) => throw new NotImplementedException();
public override void SetRecordTarget(Score score) => throw new NotImplementedException();
public override void RequestResume(Action continueResume) => throw new NotImplementedException();
public override void CancelResume() => throw new NotImplementedException();
}
public class TestHitObject : HitObject
{
public TestHitObject(HitWindows hitWindows)
{
HitWindows = hitWindows;
HitWindows.SetDifficulty(0.5f);
}
public new void AddNested(HitObject nested) => base.AddNested(nested);
}
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Users;
@ -50,7 +51,10 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("create room initially in gameplay", () =>
{
Room.RoomID.Value = null;
var newRoom = new Room();
newRoom.CopyFrom(SelectedRoom.Value);
newRoom.RoomID.Value = null;
Client.RoomSetupAction = room =>
{
room.State = MultiplayerRoomState.Playing;
@ -61,7 +65,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
});
};
RoomManager.CreateRoom(Room);
RoomManager.CreateRoom(newRoom);
});
AddUntilStep("wait for room join", () => Client.Room != null);

View File

@ -31,32 +31,24 @@ namespace osu.Game.Tests.OnlinePlay
}
[Test]
public void TestMasterClockStartsWhenAllPlayerClocksHaveFrames()
public void TestPlayerClocksStartWhenAllHaveFrames()
{
setWaiting(() => player1, false);
assertMasterState(false);
assertPlayerClockState(() => player1, false);
assertPlayerClockState(() => player2, false);
setWaiting(() => player2, false);
assertMasterState(true);
assertPlayerClockState(() => player1, true);
assertPlayerClockState(() => player2, true);
}
[Test]
public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime()
{
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
assertMasterState(false);
}
[Test]
public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime()
public void TestReadyPlayersStartWhenReadyForMaximumDelayTime()
{
setWaiting(() => player1, false);
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
assertMasterState(true);
assertPlayerClockState(() => player1, true);
assertPlayerClockState(() => player2, false);
}
[Test]
@ -153,9 +145,6 @@ namespace osu.Game.Tests.OnlinePlay
private void setPlayerClockTime(Func<TestSpectatorPlayerClock> playerClock, double offsetFromMaster)
=> AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
private void assertMasterState(bool running)
=> AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running);
private void assertCatchingUp(Func<TestSpectatorPlayerClock> playerClock, bool catchingUp) =>
AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
@ -201,6 +190,11 @@ namespace osu.Game.Tests.OnlinePlay
private class TestManualClock : ManualClock, IAdjustableClock
{
public TestManualClock()
{
IsRunning = true;
}
public void Start() => IsRunning = true;
public void Stop() => IsRunning = false;

View File

@ -0,0 +1,11 @@
#include "sh_Utils.h"
varying mediump vec2 v_TexCoord;
varying mediump vec4 v_TexRect;
void main(void)
{
float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
gl_FragColor = hsv2rgb(vec4(hueValue, 1, 1, 1));
}

View File

@ -0,0 +1,31 @@
#include "sh_Utils.h"
attribute highp vec2 m_Position;
attribute lowp vec4 m_Colour;
attribute mediump vec2 m_TexCoord;
attribute mediump vec4 m_TexRect;
attribute mediump vec2 m_BlendRange;
varying highp vec2 v_MaskingPosition;
varying lowp vec4 v_Colour;
varying mediump vec2 v_TexCoord;
varying mediump vec4 v_TexRect;
varying mediump vec2 v_BlendRange;
uniform highp mat4 g_ProjMatrix;
uniform highp mat3 g_ToMaskingSpace;
void main(void)
{
// Transform from screen space to masking space.
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z;
v_Colour = m_Colour;
v_TexCoord = m_TexCoord;
v_TexRect = m_TexRect;
v_BlendRange = m_BlendRange;
gl_Position = gProjMatrix * vec4(m_Position, 1.0, 1.0);
}

View File

@ -13,8 +13,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
@ -31,12 +33,14 @@ namespace osu.Game.Tests.Rulesets
DrawableWithDependencies drawable = null;
TestTextureStore textureStore = null;
TestSampleStore sampleStore = null;
TestShaderManager shaderManager = null;
AddStep("add dependencies", () =>
{
Child = drawable = new DrawableWithDependencies();
textureStore = drawable.ParentTextureStore;
sampleStore = drawable.ParentSampleStore;
shaderManager = drawable.ParentShaderManager;
});
AddStep("clear children", Clear);
@ -52,12 +56,14 @@ namespace osu.Game.Tests.Rulesets
AddAssert("parent texture store not disposed", () => !textureStore.IsDisposed);
AddAssert("parent sample store not disposed", () => !sampleStore.IsDisposed);
AddAssert("parent shader manager not disposed", () => !shaderManager.IsDisposed);
}
private class DrawableWithDependencies : CompositeDrawable
{
public TestTextureStore ParentTextureStore { get; private set; }
public TestSampleStore ParentSampleStore { get; private set; }
public TestShaderManager ParentShaderManager { get; private set; }
public DrawableWithDependencies()
{
@ -70,6 +76,7 @@ namespace osu.Game.Tests.Rulesets
dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore());
dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore());
dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager());
return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
}
@ -135,5 +142,23 @@ namespace osu.Game.Tests.Rulesets
public int PlaybackConcurrency { get; set; }
}
private class TestShaderManager : ShaderManager
{
public TestShaderManager()
: base(new ResourceStore<byte[]>())
{
}
public override byte[] LoadRaw(string name) => null;
public bool IsDisposed { get; private set; }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
IsDisposed = true;
}
}
}
}

View File

@ -2,14 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.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.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Rulesets;
using osu.Game.Skinning;
@ -18,14 +19,21 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Rulesets
{
[HeadlessTest]
public class TestSceneRulesetSkinProvidingContainer : OsuTestScene
{
private SkinRequester requester;
protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset();
[Cached(typeof(ISkinSource))]
private readonly ISkinSource testSource = new TestSkinProvider();
[Test]
public void TestRulesetResources()
{
setupProviderStep();
AddAssert("ruleset texture retrieved via skin", () => requester.GetTexture("test-image") != null);
AddAssert("ruleset sample retrieved via skin", () => requester.GetSample(new SampleInfo("test-sample")) != null);
}
[Test]
public void TestEarlyAddedSkinRequester()
@ -38,7 +46,7 @@ namespace osu.Game.Tests.Rulesets
rulesetSkinProvider.Add(requester = new SkinRequester());
requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture(TestSkinProvider.TEXTURE_NAME);
requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("test-image");
Child = rulesetSkinProvider;
});
@ -46,6 +54,15 @@ namespace osu.Game.Tests.Rulesets
AddAssert("requester got correct initial texture", () => textureOnLoad != null);
}
private void setupProviderStep()
{
AddStep("setup provider", () =>
{
Child = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin)
.WithChild(requester = new SkinRequester());
});
}
private class SkinRequester : Drawable, ISkin
{
private ISkinSource skin;
@ -68,28 +85,5 @@ namespace osu.Game.Tests.Rulesets
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup);
}
private class TestSkinProvider : ISkinSource
{
public const string TEXTURE_NAME = "some-texture";
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => componentName == TEXTURE_NAME ? Texture.WhitePixel : null;
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
public event Action SourceChanged
{
add { }
remove { }
}
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
public IEnumerable<ISkin> AllSources => new[] { this };
}
}
}

View File

@ -0,0 +1,94 @@
// 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.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Skins
{
[HeadlessTest]
public class TestSceneSkinProvidingContainer : OsuTestScene
{
/// <summary>
/// Ensures that the first inserted skin after resetting (via source change)
/// is always prioritised over others when providing the same resource.
/// </summary>
[Test]
public void TestPriorityPreservation()
{
TestSkinProvidingContainer provider = null;
TestSkin mostPrioritisedSource = null;
AddStep("setup sources", () =>
{
var sources = new List<TestSkin>();
for (int i = 0; i < 10; i++)
sources.Add(new TestSkin());
mostPrioritisedSource = sources.First();
Child = provider = new TestSkinProvidingContainer(sources);
});
AddAssert("texture provided by expected skin", () =>
{
return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
});
AddStep("trigger source change", () => provider.TriggerSourceChanged());
AddAssert("texture still provided by expected skin", () =>
{
return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
});
}
private class TestSkinProvidingContainer : SkinProvidingContainer
{
private readonly IEnumerable<ISkin> sources;
public TestSkinProvidingContainer(IEnumerable<ISkin> sources)
{
this.sources = sources;
}
public new void TriggerSourceChanged() => base.TriggerSourceChanged();
protected override void OnSourceChanged()
{
ResetSources();
sources.ForEach(AddSource);
}
}
private class TestSkin : ISkin
{
public const string TEXTURE_NAME = "virtual-texture";
public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException();
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
if (componentName == TEXTURE_NAME)
return Texture.WhitePixel;
return null;
}
public ISample GetSample(ISampleInfo sampleInfo) => throw new System.NotImplementedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new System.NotImplementedException();
}
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
@ -45,6 +46,14 @@ namespace osu.Game.Tests.Testing
Dependencies.Get<ISampleStore>().Get(@"test-sample") != null);
}
[Test]
public void TestRetrieveShader()
{
AddAssert("ruleset shaders retrieved", () =>
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestVertex.vs") != null &&
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestFragment.fs") != null);
}
[Test]
public void TestResolveConfigManager()
{

View File

@ -11,7 +11,6 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -36,18 +35,6 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
double? time = null;
AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
// test seek via keyboard
AddStep("seek with right arrow key", () => InputManager.Key(Key.Right));
AddAssert("time seeked forward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime > time + 2000);
AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
AddStep("seek with left arrow key", () => InputManager.Key(Key.Left));
AddAssert("time seeked backward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < time);
seekToBreak(0);
seekToBreak(1);

View File

@ -114,11 +114,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public bool ResultsCreated { get; private set; }
public FakeRankingPushPlayer()
: base(true, true)
{
}
protected override ResultsScreen CreateResults(ScoreInfo score)
{
var results = base.CreateResults(score);

View File

@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Gameplay
showOverlay();
AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected.Value);
AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().State == SelectionState.Selected);
}
/// <summary>
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Gameplay
showOverlay();
AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("First button selected", () => getButton(0).Selected.Value);
AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected);
}
/// <summary>
@ -111,11 +111,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Show overlay", () => failOverlay.Show());
AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value);
AddAssert("Last button selected", () => failOverlay.Buttons.Last().State == SelectionState.Selected);
AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value);
AddAssert("First button selected", () => failOverlay.Buttons.First().State == SelectionState.Selected);
AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value);
AddAssert("Last button selected", () => failOverlay.Buttons.Last().State == SelectionState.Selected);
}
/// <summary>
@ -127,11 +127,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Show overlay", () => failOverlay.Show());
AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value);
AddAssert("First button selected", () => failOverlay.Buttons.First().State == SelectionState.Selected);
AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value);
AddAssert("Last button selected", () => failOverlay.Buttons.Last().State == SelectionState.Selected);
AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value);
AddAssert("First button selected", () => failOverlay.Buttons.First().State == SelectionState.Selected);
}
/// <summary>
@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hover first button", () => InputManager.MoveMouseTo(failOverlay.Buttons.First()));
AddStep("Hide overlay", () => failOverlay.Hide());
AddAssert("Overlay state is reset", () => !failOverlay.Buttons.Any(b => b.Selected.Value));
AddAssert("Overlay state is reset", () => failOverlay.Buttons.All(b => b.State == SelectionState.NotSelected));
}
/// <summary>
@ -162,11 +162,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hide overlay", () => pauseOverlay.Hide());
showOverlay();
AddAssert("First button not selected", () => !getButton(0).Selected.Value);
AddAssert("First button not selected", () => getButton(0).State == SelectionState.NotSelected);
AddStep("Move slightly", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(1)));
AddAssert("First button selected", () => getButton(0).Selected.Value);
AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected);
}
/// <summary>
@ -179,8 +179,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1)));
AddAssert("First button not selected", () => !getButton(0).Selected.Value);
AddAssert("Second button selected", () => getButton(1).Selected.Value);
AddAssert("First button not selected", () => getButton(0).State == SelectionState.NotSelected);
AddAssert("Second button selected", () => getButton(1).State == SelectionState.Selected);
}
/// <summary>
@ -196,8 +196,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1)));
AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("Second button not selected", () => !getButton(1).Selected.Value);
AddAssert("First button selected", () => getButton(0).Selected.Value);
AddAssert("Second button not selected", () => getButton(1).State == SelectionState.NotSelected);
AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected);
}
/// <summary>
@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1)));
AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero));
AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("First button selected", () => getButton(0).Selected.Value); // Initial state condition
AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected); // Initial state condition
}
/// <summary>
@ -282,7 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay
showOverlay();
AddAssert("No button selected",
() => pauseOverlay.Buttons.All(button => !button.Selected.Value));
() => pauseOverlay.Buttons.All(button => button.State == SelectionState.NotSelected));
}
private void showOverlay() => AddStep("Show overlay", () => pauseOverlay.Show());

View File

@ -0,0 +1,246 @@
// 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.Screens;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestScenePlayerScoreSubmission : PlayerTestScene
{
protected override bool AllowFail => allowFail;
private bool allowFail;
private Func<RulesetInfo, IBeatmap> createCustomBeatmap;
private Func<Ruleset> createCustomRuleset;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
protected override bool HasCustomSteps => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => createCustomBeatmap?.Invoke(ruleset) ?? createTestBeatmap(ruleset);
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
{
var beatmap = (TestBeatmap)base.CreateBeatmap(ruleset);
beatmap.HitObjects = beatmap.HitObjects.Take(10).ToList();
return beatmap;
}
[Test]
public void TestNoSubmissionOnResultsWithNoToken()
{
prepareTokenResponse(false);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addFakeHit();
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestSubmissionOnResults()
{
prepareTokenResponse(true);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addFakeHit();
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
}
[Test]
public void TestNoSubmissionOnExitWithNoToken()
{
prepareTokenResponse(false);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addFakeHit();
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestNoSubmissionOnEmptyFail()
{
prepareTokenResponse(true);
createPlayerTest(true);
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestSubmissionOnFail()
{
prepareTokenResponse(true);
createPlayerTest(true);
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
addFakeHit();
AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("exit", () => Player.Exit());
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
}
[Test]
public void TestNoSubmissionOnEmptyExit()
{
prepareTokenResponse(true);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestSubmissionOnExit()
{
prepareTokenResponse(true);
createPlayerTest();
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
addFakeHit();
AddStep("exit", () => Player.Exit());
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
}
[Test]
public void TestNoSubmissionOnLocalBeatmap()
{
prepareTokenResponse(true);
createPlayerTest(false, r =>
{
var beatmap = createTestBeatmap(r);
beatmap.BeatmapInfo.OnlineBeatmapID = null;
return beatmap;
});
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
addFakeHit();
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test]
public void TestNoSubmissionOnCustomRuleset()
{
prepareTokenResponse(true);
createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = 10 } });
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
addFakeHit();
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
private void createPlayerTest(bool allowFail = false, Func<RulesetInfo, IBeatmap> createBeatmap = null, Func<Ruleset> createRuleset = null)
{
CreateTest(() => AddStep("set up requirements", () =>
{
this.allowFail = allowFail;
createCustomBeatmap = createBeatmap;
createCustomRuleset = createRuleset;
}));
}
private void prepareTokenResponse(bool validToken)
{
AddStep("Prepare test API", () =>
{
dummyAPI.HandleRequest = request =>
{
switch (request)
{
case CreateSoloScoreRequest tokenRequest:
if (validToken)
tokenRequest.TriggerSuccess(new APIScoreToken { ID = 1234 });
else
tokenRequest.TriggerFailure(new APIException("something went wrong!", null));
return true;
}
return false;
};
});
}
private void addFakeHit()
{
AddUntilStep("wait for first result", () => Player.Results.Count > 0);
AddStep("force successfuly hit", () =>
{
Player.ScoreProcessor.RevertResult(Player.Results.First());
Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new OsuJudgement())
{
Type = HitResult.Great,
});
});
}
}
}

View File

@ -0,0 +1,87 @@
// 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.Rulesets;
using osu.Game.Rulesets.Osu;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneReplayPlayer : RateAdjustedBeatmapTestScene
{
protected TestReplayPlayer Player;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("Initialise player", () => Player = CreatePlayer(new OsuRuleset()));
AddStep("Load player", () => LoadScreen(Player));
AddUntilStep("player loaded", () => Player.IsLoaded);
}
[Test]
public void TestPause()
{
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddStep("Pause playback", () => InputManager.Key(Key.Space));
AddUntilStep("Time stopped progressing", () =>
{
double current = Player.GameplayClockContainer.CurrentTime;
bool changed = lastTime != current;
lastTime = current;
return !changed;
});
AddWaitStep("wait some", 10);
AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
}
[Test]
public void TestSeekBackwards()
{
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddStep("Seek backwards", () =>
{
lastTime = Player.GameplayClockContainer.CurrentTime;
InputManager.Key(Key.Left);
});
AddAssert("Jumped backwards", () => Player.GameplayClockContainer.CurrentTime - lastTime < 0);
}
[Test]
public void TestSeekForwards()
{
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddStep("Seek forwards", () =>
{
lastTime = Player.GameplayClockContainer.CurrentTime;
InputManager.Key(Key.Right);
});
AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500);
}
protected TestReplayPlayer CreatePlayer(Ruleset ruleset)
{
Beatmap.Value = CreateWorkingBeatmap(ruleset.RulesetInfo);
SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
return new TestReplayPlayer(false);
}
}
}

View File

@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Children = new[]
{
new ExposedSkinnableDrawable("default", _ => new DefaultBox()),
new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.ScaleToFit),
new ExposedSkinnableDrawable("available", _ => new DefaultBox()),
new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.NoScaling)
}
},
@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void Disable()
{
allow = false;
OnSourceChanged();
TriggerSourceChanged();
}
public SwitchableSkinProvidingContainer(ISkin skin)

View File

@ -2,8 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -41,8 +39,6 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private OsuGameBase game { get; set; }
private int nextFrame;
private BeatmapSetInfo importedBeatmap;
private int importedBeatmapId;
@ -51,8 +47,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
base.SetUpSteps();
AddStep("reset sent frames", () => nextFrame = 0);
AddStep("import beatmap", () =>
{
importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result;
@ -105,7 +99,8 @@ namespace osu.Game.Tests.Visual.Gameplay
waitForPlayer();
checkPaused(true);
sendFrames(1000); // send enough frames to ensure play won't be paused
// send enough frames to ensure play won't be paused
sendFrames(100);
checkPaused(false);
}
@ -114,12 +109,12 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestSpectatingDuringGameplay()
{
start();
sendFrames(300);
loadSpectatingScreen();
waitForPlayer();
AddStep("advance frame count", () => nextFrame = 300);
sendFrames();
sendFrames(300);
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime > 30000);
}
@ -220,11 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void sendFrames(int count = 10)
{
AddStep("send frames", () =>
{
testSpectatorClient.SendFrames(streamingUser.Id, nextFrame, count);
nextFrame += count;
});
AddStep("send frames", () => testSpectatorClient.SendFrames(streamingUser.Id, count));
}
private void loadSpectatingScreen()
@ -232,14 +223,5 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser)));
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
}
internal class TestUserLookupCache : UserLookupCache
{
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User
{
Id = lookup,
Username = $"User {lookup}"
});
}
}
}

View File

@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var legacyFrame in frames.Frames)
{
var frame = new TestReplayFrame();
frame.FromLegacy(legacyFrame, null, null);
frame.FromLegacy(legacyFrame, null);
replay.Frames.Add(frame);
}
}

View File

@ -1,61 +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 osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public abstract class RoomManagerTestScene : RoomTestScene
{
[Cached(Type = typeof(IRoomManager))]
protected TestRoomManager RoomManager { get; } = new TestRoomManager();
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("clear rooms", () => RoomManager.Rooms.Clear());
}
protected void AddRooms(int count, RulesetInfo ruleset = null)
{
AddStep("add rooms", () =>
{
for (int i = 0; i < count; i++)
{
var room = new Room
{
RoomID = { Value = i },
Name = { Value = $"Room {i}" },
Host = { Value = new User { Username = "Host" } },
EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) },
Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }
};
if (ruleset != null)
{
room.Playlist.Add(new PlaylistItem
{
Ruleset = { Value = ruleset },
Beatmap =
{
Value = new BeatmapInfo
{
Metadata = new BeatmapMetadata()
}
}
});
}
RoomManager.Rooms.Add(room);
}
});
}
}
}

View File

@ -1,35 +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 osu.Framework.Bindables;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestRoomManager : IRoomManager
{
public event Action RoomsUpdated
{
add { }
remove { }
}
public readonly BindableList<Room> Rooms = new BindableList<Room>();
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
IBindableList<Room> IRoomManager.Rooms => Rooms;
public void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) => Rooms.Add(room);
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{
}
public void PartRoom()
{
}
}
}

View File

@ -4,17 +4,21 @@
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneLoungeRoomInfo : RoomTestScene
public class TestSceneLoungeRoomInfo : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room();
Child = new RoomInfo
{
Anchor = Anchor.Centre,
@ -23,15 +27,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
});
public override void SetUpSteps()
{
// Todo: Temp
}
[Test]
public void TestNonSelectedRoom()
{
AddStep("set null room", () => Room.RoomID.Value = null);
AddStep("set null room", () => SelectedRoom.Value.RoomID.Value = null);
}
[Test]
@ -39,11 +38,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("set open room", () =>
{
Room.RoomID.Value = 0;
Room.Name.Value = "Room 0";
Room.Host.Value = new User { Username = "peppy", Id = 2 };
Room.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
Room.Status.Value = new RoomStatusOpen();
SelectedRoom.Value.RoomID.Value = 0;
SelectedRoom.Value.Name.Value = "Room 0";
SelectedRoom.Value.Host.Value = new User { Username = "peppy", Id = 2 };
SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
SelectedRoom.Value.Status.Value = new RoomStatusOpen();
});
}
}

View File

@ -3,24 +3,26 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneLoungeRoomsContainer : RoomManagerTestScene
public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene
{
protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager;
private RoomsContainer container;
[BackgroundDependencyLoader]
private void load()
[SetUp]
public new void Setup() => Schedule(() =>
{
Child = container = new RoomsContainer
{
@ -29,12 +31,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
Width = 0.5f,
JoinRequested = joinRequested
};
}
});
[Test]
public void TestBasicListChanges()
{
AddRooms(3);
AddStep("add rooms", () => RoomManager.AddRooms(3));
AddAssert("has 3 rooms", () => container.Rooms.Count == 3);
AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault()));
@ -51,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestKeyboardNavigation()
{
AddRooms(3);
AddStep("add rooms", () => RoomManager.AddRooms(3));
AddAssert("no selection", () => checkRoomSelected(null));
@ -72,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestClickDeselection()
{
AddRooms(1);
AddStep("add room", () => RoomManager.AddRooms(1));
AddAssert("no selection", () => checkRoomSelected(null));
@ -91,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestStringFiltering()
{
AddRooms(4);
AddStep("add rooms", () => RoomManager.AddRooms(4));
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
@ -107,21 +109,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRulesetFiltering()
{
AddRooms(2, new OsuRuleset().RulesetInfo);
AddRooms(3, new CatchRuleset().RulesetInfo);
AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo));
AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo));
// Todo: What even is this case...?
AddStep("set empty filter criteria", () => container.Filter(null));
AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo }));
AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
AddStep("filter catch rooms", () => container.Filter(new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo }));
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
private bool checkRoomSelected(Room room) => Room == room;
private bool checkRoomSelected(Room room) => SelectedRoom.Value == room;
private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus();

View File

@ -11,11 +11,12 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchBeatmapDetailArea : RoomTestScene
public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
{
[Resolved]
private BeatmapManager beatmapManager { get; set; }
@ -26,6 +27,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room();
Child = new MatchBeatmapDetailArea
{
Anchor = Anchor.Centre,
@ -37,9 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createNewItem()
{
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
ID = Room.Playlist.Count,
ID = SelectedRoom.Value.Playlist.Count,
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
RequiredMods =

View File

@ -7,46 +7,49 @@ using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchHeader : RoomTestScene
public class TestSceneMatchHeader : OnlinePlayTestScene
{
public TestSceneMatchHeader()
{
Child = new Header();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value = new Room
{
Beatmap =
Name = { Value = "A very awesome room" },
Host = { Value = new User { Id = 2, Username = "peppy" } },
Playlist =
{
Value = new BeatmapInfo
new PlaylistItem
{
Metadata = new BeatmapMetadata
Beatmap =
{
Title = "Title",
Artist = "Artist",
AuthorString = "Author",
Value = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Title = "Title",
Artist = "Artist",
AuthorString = "Author",
},
Version = "Version",
Ruleset = new OsuRuleset().RulesetInfo
}
},
Version = "Version",
Ruleset = new OsuRuleset().RulesetInfo
RequiredMods =
{
new OsuModDoubleTime(),
new OsuModNoFail(),
new OsuModRelax(),
}
}
},
RequiredMods =
{
new OsuModDoubleTime(),
new OsuModNoFail(),
new OsuModRelax(),
}
});
};
Room.Name.Value = "A very awesome room";
Room.Host.Value = new User { Id = 2, Username = "peppy" };
Child = new Header();
});
}
}

View File

@ -2,72 +2,74 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchLeaderboard : RoomTestScene
public class TestSceneMatchLeaderboard : OnlinePlayTestScene
{
protected override bool UseOnlineAPI => true;
public TestSceneMatchLeaderboard()
{
Add(new MatchLeaderboard
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = MatchLeaderboardScope.Overall,
});
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
private void load()
{
var req = new GetRoomScoresRequest();
req.Success += v => { };
req.Failure += _ => { };
((DummyAPIAccess)API).HandleRequest = r =>
{
switch (r)
{
case GetRoomLeaderboardRequest leaderboardRequest:
leaderboardRequest.TriggerSuccess(new APILeaderboard
{
Leaderboard = new List<APIUserScoreAggregate>
{
new APIUserScoreAggregate
{
UserID = 2,
User = new User { Id = 2, Username = "peppy" },
TotalScore = 995533,
RoomID = 3,
CompletedBeatmaps = 1,
TotalAttempts = 6,
Accuracy = 0.9851
},
new APIUserScoreAggregate
{
UserID = 1040328,
User = new User { Id = 1040328, Username = "smoogipoo" },
TotalScore = 981100,
RoomID = 3,
CompletedBeatmaps = 1,
TotalAttempts = 9,
Accuracy = 0.937
}
}
});
return true;
}
api.Queue(req);
return false;
};
}
[SetUp]
public new void Setup() => Schedule(() =>
{
Room.RoomID.Value = 3;
SelectedRoom.Value = new Room { RoomID = { Value = 3 } };
Child = new MatchLeaderboard
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = MatchLeaderboardScope.Overall,
};
});
private class GetRoomScoresRequest : APIRequest<List<RoomScore>>
{
protected override string Target => "rooms/3/leaderboard";
}
private class RoomScore
{
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("total_score")]
public int TotalScore { get; set; }
[JsonProperty("pp")]
public double PP { get; set; }
[JsonProperty("attempts")]
public int TotalAttempts { get; set; }
[JsonProperty("completed")]
public int CompletedAttempts { get; set; }
}
}
}

View File

@ -3,74 +3,40 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Database;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
{
[Cached(typeof(SpectatorClient))]
private TestSpectatorClient spectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly Dictionary<int, ManualClock> clocks = new Dictionary<int, ManualClock>
{
{ PLAYER_1_ID, new ManualClock() },
{ PLAYER_2_ID, new ManualClock() }
};
public TestSceneMultiSpectatorLeaderboard()
{
base.Content.AddRange(new Drawable[]
{
spectatorClient,
lookupCache,
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
private Dictionary<int, ManualClock> clocks;
private MultiSpectatorLeaderboard leaderboard;
[SetUpSteps]
public new void SetUpSteps()
{
MultiSpectatorLeaderboard leaderboard = null;
AddStep("reset", () =>
{
Clear();
foreach (var (userId, clock) in clocks)
clocks = new Dictionary<int, ManualClock>
{
spectatorClient.EndPlay(userId);
clock.CurrentTime = 0;
}
{ PLAYER_1_ID, new ManualClock() },
{ PLAYER_2_ID, new ManualClock() }
};
foreach (var (userId, _) in clocks)
SpectatorClient.StartPlay(userId, 0);
});
AddStep("create leaderboard", () =>
{
foreach (var (userId, _) in clocks)
spectatorClient.StartPlay(userId, 0);
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable);
@ -96,10 +62,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
// For player 2, send frames in sets of 10.
for (int i = 0; i < 100; i++)
{
spectatorClient.SendFrames(PLAYER_1_ID, i, 1);
SpectatorClient.SendFrames(PLAYER_1_ID, 1);
if (i % 10 == 0)
spectatorClient.SendFrames(PLAYER_2_ID, i, 10);
SpectatorClient.SendFrames(PLAYER_2_ID, 10);
}
});
@ -145,17 +111,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void assertCombo(int userId, int expectedCombo)
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
private class TestUserLookupCache : UserLookupCache
{
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
{
return Task.FromResult(new User
{
Id = lookup,
Username = $"User {lookup}"
});
}
}
}
}

View File

@ -3,31 +3,20 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
{
[Cached(typeof(SpectatorClient))]
private TestSpectatorClient spectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
[Resolved]
private OsuGameBase game { get; set; }
@ -37,7 +26,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiSpectatorScreen spectatorScreen;
private readonly List<int> playingUserIds = new List<int>();
private readonly Dictionary<int, int> nextFrame = new Dictionary<int, int>();
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
@ -51,25 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedBeatmapId = importedBeatmap.OnlineBeatmapID ?? -1;
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("reset sent frames", () => nextFrame.Clear());
AddStep("add streaming client", () =>
{
Remove(spectatorClient);
Add(spectatorClient);
});
AddStep("finish previous gameplay", () =>
{
foreach (var id in playingUserIds)
spectatorClient.EndPlay(id);
playingUserIds.Clear();
});
}
[SetUp]
public new void Setup() => Schedule(() => playingUserIds.Clear());
[Test]
public void TestDelayedStart()
@ -80,18 +51,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID);
playingUserIds.Add(PLAYER_1_ID);
playingUserIds.Add(PLAYER_2_ID);
nextFrame[PLAYER_1_ID] = 0;
nextFrame[PLAYER_2_ID] = 0;
});
loadSpectateScreen(false);
AddWaitStep("wait a bit", 10);
AddStep("load player first_player_id", () => spectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
AddStep("load player first_player_id", () => SpectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1);
AddWaitStep("wait a bit", 10);
AddStep("load player second_player_id", () => spectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
AddStep("load player second_player_id", () => SpectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
}
@ -107,6 +76,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 20);
}
[Test]
public void TestTimeDoesNotProgressWhileAllPlayersPaused()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();
sendFrames(PLAYER_1_ID, 40);
sendFrames(PLAYER_2_ID, 20);
checkPaused(PLAYER_2_ID, true);
checkPausedInstant(PLAYER_1_ID, false);
AddAssert("master clock still running", () => this.ChildrenOfType<MasterGameplayClockContainer>().Single().IsRunning);
checkPaused(PLAYER_1_ID, true);
AddUntilStep("master clock paused", () => !this.ChildrenOfType<MasterGameplayClockContainer>().Single().IsRunning);
}
[Test]
public void TestPlayersMustStartSimultaneously()
{
@ -151,7 +137,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// Send initial frames for both players. A few more for player 1.
sendFrames(PLAYER_1_ID, 20);
sendFrames(PLAYER_2_ID, 10);
sendFrames(PLAYER_2_ID);
checkPausedInstant(PLAYER_1_ID, false);
checkPausedInstant(PLAYER_2_ID, false);
@ -182,7 +168,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// Send initial frames for both players. A few more for player 1.
sendFrames(PLAYER_1_ID, 1000);
sendFrames(PLAYER_2_ID, 10);
sendFrames(PLAYER_2_ID, 30);
checkPausedInstant(PLAYER_1_ID, false);
checkPausedInstant(PLAYER_2_ID, false);
@ -208,10 +194,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, true);
sendFrames(PLAYER_1_ID, 10);
sendFrames(PLAYER_1_ID);
sendFrames(PLAYER_2_ID, 20);
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
checkPaused(PLAYER_1_ID, false);
assertOneNotMuted();
checkPaused(PLAYER_1_ID, true);
assertMuted(PLAYER_1_ID, true);
@ -229,6 +215,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertMuted(PLAYER_2_ID, true);
}
[Test]
public void TestSpectatingDuringGameplay()
{
var players = new[] { PLAYER_1_ID, PLAYER_2_ID };
start(players);
sendFrames(players, 300);
loadSpectateScreen();
sendFrames(players, 300);
AddUntilStep("playing from correct point in time", () => this.ChildrenOfType<DrawableRuleset>().All(r => r.FrameStableClock.CurrentTime > 30000));
}
[Test]
public void TestSpectatingDuringGameplayWithLateFrames()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
sendFrames(new[] { PLAYER_1_ID, PLAYER_2_ID }, 300);
loadSpectateScreen();
sendFrames(PLAYER_1_ID, 300);
AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
checkPaused(PLAYER_1_ID, false);
sendFrames(PLAYER_2_ID, 300);
AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType<DrawableRuleset>().Single().FrameStableClock.CurrentTime > 30000);
}
private void loadSpectateScreen(bool waitForPlayerLoad = true)
{
AddStep("load screen", () =>
@ -242,8 +258,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
}
private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
private void start(int[] userIds, int? beatmapId = null)
{
AddStep("start play", () =>
@ -251,23 +265,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (int id in userIds)
{
Client.CurrentMatchPlayingUserIds.Add(id);
spectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id);
nextFrame[id] = 0;
}
});
}
private void finish(int userId)
{
AddStep("end play", () =>
{
spectatorClient.EndPlay(userId);
playingUserIds.Remove(userId);
nextFrame.Remove(userId);
});
}
private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count);
private void sendFrames(int[] userIds, int count = 10)
@ -275,10 +278,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("send frames", () =>
{
foreach (int id in userIds)
{
spectatorClient.SendFrames(id, nextFrame[id], count);
nextFrame[id] += count;
}
SpectatorClient.SendFrames(id, count);
});
}
@ -286,7 +286,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
private void checkPausedInstant(int userId, bool state)
=> AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
{
checkPaused(userId, state);
// Todo: The following should work, but is broken because SpectatorScreen retrieves the WorkingBeatmap via the BeatmapManager, bypassing the test scene clock and running real-time.
// AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
}
private void assertOneNotMuted() => AddAssert("one player not muted", () => spectatorScreen.ChildrenOfType<PlayerArea>().Count(p => !p.Mute) == 1);
private void assertMuted(int userId, bool muted)
=> AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted);
@ -297,17 +304,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType<Player>().Single();
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
internal class TestUserLookupCache : UserLookupCache
{
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
{
return Task.FromResult(new User
{
Id = lookup,
Username = $"User {lookup}"
});
}
}
}
}

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