mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 02:49:53 +08:00
Compare commits
654 Commits
@@ -15,6 +15,7 @@ jobs:
|
||||
- { prettyname: macOS, fullname: macos-latest }
|
||||
- { prettyname: Linux, fullname: ubuntu-latest }
|
||||
threadingMode: ['SingleThread', 'MultiThreaded']
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
- { prettyname: macOS }
|
||||
- { prettyname: Linux }
|
||||
threadingMode: ['SingleThread', 'MultiThreaded']
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Annotate CI run with test results
|
||||
uses: dorny/test-reporter@v1.4.2
|
||||
|
||||
@@ -336,3 +336,6 @@ inspectcode
|
||||
/BenchmarkDotNet.Artifacts
|
||||
|
||||
*.GeneratedMSBuildEditorConfig.editorconfig
|
||||
|
||||
# Fody (pulled in by Realm) - schema file
|
||||
FodyWeavers.xsd
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="osu-client-sqlite" uuid="1aa4b9be-cd8d-47ae-8186-30a13cd724a5">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db</jdbc-url>
|
||||
<driver-properties>
|
||||
<property name="enable_load_extension" value="true" />
|
||||
</driver-properties>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Realm DisableAnalytics="true" />
|
||||
</Weavers>
|
||||
@@ -43,7 +43,7 @@ If your platform is not listed above, there is still a chance you can manually b
|
||||
|
||||
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
|
||||
|
||||
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
|
||||
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
|
||||
|
||||
## Developing osu!
|
||||
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+6
-2
@@ -51,7 +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.616.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.701.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.702.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.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace osu.Desktop.Updater
|
||||
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 +141,7 @@ namespace osu.Desktop.Updater
|
||||
Activated = () =>
|
||||
{
|
||||
updateManager.PrepareUpdateAsync()
|
||||
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
|
||||
.ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit()));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneEditor : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new CatchRuleset();
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
@@ -31,10 +31,23 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
private Container<CaughtObject> droppedObjectContainer;
|
||||
[Cached]
|
||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
private readonly Container trailContainer;
|
||||
|
||||
private TestCatcher catcher;
|
||||
|
||||
public TestSceneCatcher()
|
||||
{
|
||||
Add(trailContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Depth = -1
|
||||
});
|
||||
Add(droppedObjectContainer = new DroppedObjectContainer());
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
@@ -43,20 +56,13 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
CircleSize = 0,
|
||||
};
|
||||
|
||||
var trailContainer = new Container();
|
||||
droppedObjectContainer = new Container<CaughtObject>();
|
||||
catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
|
||||
if (catcher != null)
|
||||
Remove(catcher);
|
||||
|
||||
Child = new Container
|
||||
Add(catcher = new TestCatcher(trailContainer, difficulty)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
trailContainer,
|
||||
droppedObjectContainer,
|
||||
catcher
|
||||
}
|
||||
};
|
||||
Anchor = Anchor.Centre
|
||||
});
|
||||
});
|
||||
|
||||
[Test]
|
||||
@@ -293,8 +299,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
|
||||
|
||||
public TestCatcher(Container trailsTarget, Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty)
|
||||
: base(trailsTarget, droppedObjectTarget, difficulty)
|
||||
public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty)
|
||||
: base(trailsTarget, difficulty)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
@@ -97,18 +96,12 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
SetContents(_ =>
|
||||
{
|
||||
var droppedObjectContainer = new Container<CaughtObject>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
|
||||
return new CatchInputManager(catchRuleset)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
droppedObjectContainer,
|
||||
new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
|
||||
new TestCatcherArea(beatmapDifficulty)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
@@ -126,9 +119,13 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private class TestCatcherArea : CatcherArea
|
||||
{
|
||||
public TestCatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
|
||||
: base(droppedObjectContainer, beatmapDifficulty)
|
||||
[Cached]
|
||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
|
||||
: base(beatmapDifficulty)
|
||||
{
|
||||
AddInternal(droppedObjectContainer = new DroppedObjectContainer());
|
||||
}
|
||||
|
||||
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
|
||||
|
||||
@@ -118,11 +118,10 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
AddStep("create hyper-dashing catcher", () =>
|
||||
{
|
||||
Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container<CaughtObject>())
|
||||
Child = setupSkinHierarchy(catcherArea = new TestCatcherArea
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(4f),
|
||||
Origin = Anchor.Centre
|
||||
}, skin);
|
||||
});
|
||||
|
||||
@@ -206,5 +205,18 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class TestCatcherArea : CatcherArea
|
||||
{
|
||||
[Cached]
|
||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
public TestCatcherArea()
|
||||
{
|
||||
Scale = new Vector2(4f);
|
||||
|
||||
AddInternal(droppedObjectContainer = new DroppedObjectContainer());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.MathUtils;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
@@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
{
|
||||
public const int RNG_SEED = 1337;
|
||||
|
||||
public bool HardRockOffsets { get; set; }
|
||||
|
||||
public CatchBeatmapProcessor(IBeatmap beatmap)
|
||||
: base(beatmap)
|
||||
{
|
||||
@@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
|
||||
public void ApplyPositionOffsets(IBeatmap beatmap)
|
||||
{
|
||||
var rng = new FastRandom(RNG_SEED);
|
||||
|
||||
bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
|
||||
float? lastPosition = null;
|
||||
double lastStartTime = 0;
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
switch (obj)
|
||||
{
|
||||
case Fruit fruit:
|
||||
if (shouldApplyHardRockOffset)
|
||||
if (HardRockOffsets)
|
||||
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
|
||||
break;
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using System;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
@@ -175,12 +177,14 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
|
||||
|
||||
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
|
||||
|
||||
public int LegacyID => 2;
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class BananaShowerCompositionTool : HitObjectCompositionTool
|
||||
{
|
||||
public BananaShowerCompositionTool()
|
||||
: base(nameof(BananaShower))
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint<BananaShower>
|
||||
{
|
||||
private readonly TimeSpanOutline outline;
|
||||
|
||||
public BananaShowerPlacementBlueprint()
|
||||
{
|
||||
InternalChild = outline = new TimeSpanOutline();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
switch (PlacementActive)
|
||||
{
|
||||
case PlacementState.Waiting:
|
||||
if (e.Button != MouseButton.Left) break;
|
||||
|
||||
BeginPlacement(true);
|
||||
return true;
|
||||
|
||||
case PlacementState.Active:
|
||||
if (e.Button != MouseButton.Right) break;
|
||||
|
||||
// If the duration is negative, swap the start and the end time to make the duration positive.
|
||||
if (HitObject.Duration < 0)
|
||||
{
|
||||
HitObject.StartTime = HitObject.EndTime;
|
||||
HitObject.Duration = -HitObject.Duration;
|
||||
}
|
||||
|
||||
EndPlacement(HitObject.Duration > 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
if (!(result.Time is double time)) return;
|
||||
|
||||
switch (PlacementActive)
|
||||
{
|
||||
case PlacementState.Waiting:
|
||||
HitObject.StartTime = time;
|
||||
break;
|
||||
|
||||
case PlacementState.Active:
|
||||
HitObject.EndTime = time;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class BananaShowerSelectionBlueprint : CatchSelectionBlueprint<BananaShower>
|
||||
{
|
||||
public BananaShowerSelectionBlueprint(BananaShower hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class CatchPlacementBlueprint<THitObject> : PlacementBlueprint
|
||||
where THitObject : CatchHitObject, new()
|
||||
{
|
||||
protected new THitObject HitObject => (THitObject)base.HitObject;
|
||||
|
||||
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
|
||||
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; }
|
||||
|
||||
public CatchPlacementBlueprint()
|
||||
: base(new THitObject())
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Framework.Allocation;
|
||||
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
|
||||
{
|
||||
public abstract class CatchSelectionBlueprint<THitObject> : HitObjectSelectionBlueprint<THitObject>
|
||||
where THitObject : CatchHitObject
|
||||
{
|
||||
public override Vector2 ScreenSpaceSelectionPoint
|
||||
{
|
||||
get
|
||||
{
|
||||
float x = HitObject.OriginalX;
|
||||
float y = HitObjectContainer.PositionAtTime(HitObject.StartTime);
|
||||
return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos);
|
||||
|
||||
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
|
||||
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; }
|
||||
|
||||
protected CatchSelectionBlueprint(THitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
public class FruitOutline : CompositeDrawable
|
||||
{
|
||||
public FruitOutline()
|
||||
{
|
||||
Anchor = Anchor.BottomLeft;
|
||||
Origin = Anchor.Centre;
|
||||
Size = new Vector2(2 * CatchHitObject.OBJECT_RADIUS);
|
||||
InternalChild = new BorderPiece();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour osuColour)
|
||||
{
|
||||
Colour = osuColour.Yellow;
|
||||
}
|
||||
|
||||
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
|
||||
{
|
||||
X = hitObject.EffectiveX;
|
||||
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
|
||||
Scale = new Vector2(hitObject.Scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
public class TimeSpanOutline : CompositeDrawable
|
||||
{
|
||||
private const float border_width = 4;
|
||||
|
||||
private const float opacity_when_empty = 0.5f;
|
||||
|
||||
private bool isEmpty = true;
|
||||
|
||||
public TimeSpanOutline()
|
||||
{
|
||||
Anchor = Origin = Anchor.BottomLeft;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
Masking = true;
|
||||
BorderThickness = border_width;
|
||||
Alpha = opacity_when_empty;
|
||||
|
||||
// A box is needed to make the border visible.
|
||||
InternalChild = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Transparent
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour osuColour)
|
||||
{
|
||||
BorderColour = osuColour.Yellow;
|
||||
}
|
||||
|
||||
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, BananaShower hitObject)
|
||||
{
|
||||
float startY = hitObjectContainer.PositionAtTime(hitObject.StartTime);
|
||||
float endY = hitObjectContainer.PositionAtTime(hitObject.EndTime);
|
||||
|
||||
Y = Math.Max(startY, endY);
|
||||
float height = Math.Abs(startY - endY);
|
||||
|
||||
bool wasEmpty = isEmpty;
|
||||
isEmpty = height == 0;
|
||||
if (wasEmpty != isEmpty)
|
||||
this.FadeTo(isEmpty ? opacity_when_empty : 1f, 150);
|
||||
|
||||
Height = Math.Max(height, border_width);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class FruitPlacementBlueprint : CatchPlacementBlueprint<Fruit>
|
||||
{
|
||||
private readonly FruitOutline outline;
|
||||
|
||||
public FruitPlacementBlueprint()
|
||||
{
|
||||
InternalChild = outline = new FruitOutline();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
BeginPlacement();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left) return base.OnMouseDown(e);
|
||||
|
||||
EndPlacement(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
|
||||
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class FruitSelectionBlueprint : CatchSelectionBlueprint<Fruit>
|
||||
{
|
||||
private readonly FruitOutline outline;
|
||||
|
||||
public FruitSelectionBlueprint(Fruit hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
InternalChild = outline = new FruitOutline();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (IsSelected)
|
||||
outline.UpdateFrom(HitObjectContainer, HitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint<JuiceStream>
|
||||
{
|
||||
public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
|
||||
|
||||
private float minNestedX;
|
||||
private float maxNestedX;
|
||||
|
||||
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
HitObject.DefaultsApplied += onDefaultsApplied;
|
||||
computeObjectBounds();
|
||||
}
|
||||
|
||||
private void onDefaultsApplied(HitObject _) => computeObjectBounds();
|
||||
|
||||
private void computeObjectBounds()
|
||||
{
|
||||
minNestedX = HitObject.NestedHitObjects.OfType<CatchHitObject>().Min(nested => nested.OriginalX) - HitObject.OriginalX;
|
||||
maxNestedX = HitObject.NestedHitObjects.OfType<CatchHitObject>().Max(nested => nested.OriginalX) - HitObject.OriginalX;
|
||||
}
|
||||
|
||||
private RectangleF getBoundingBox()
|
||||
{
|
||||
float left = HitObject.OriginalX + minNestedX;
|
||||
float right = HitObject.OriginalX + maxNestedX;
|
||||
float top = HitObjectContainer.PositionAtTime(HitObject.EndTime);
|
||||
float bottom = HitObjectContainer.PositionAtTime(HitObject.StartTime);
|
||||
float objectRadius = CatchHitObject.OBJECT_RADIUS * HitObject.Scale;
|
||||
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
HitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public CatchBlueprintContainer(CatchHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new CatchSelectionHandler();
|
||||
|
||||
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case Fruit fruit:
|
||||
return new FruitSelectionBlueprint(fruit);
|
||||
|
||||
case JuiceStream juiceStream:
|
||||
return new JuiceStreamSelectionBlueprint(juiceStream);
|
||||
|
||||
case BananaShower bananaShower:
|
||||
return new BananaShowerSelectionBlueprint(bananaShower);
|
||||
}
|
||||
|
||||
return base.CreateHitObjectBlueprintFor(hitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchEditorPlayfield : CatchPlayfield
|
||||
{
|
||||
// TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
|
||||
public CatchEditorPlayfield(BeatmapDifficulty difficulty)
|
||||
: base(difficulty)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// TODO: honor "hit animation" setting?
|
||||
CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
|
||||
|
||||
// TODO: disable hit lighting as well
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchHitObjectComposer : HitObjectComposer<CatchHitObject>
|
||||
{
|
||||
public CatchHitObjectComposer(CatchRuleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
}
|
||||
|
||||
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
|
||||
new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
{
|
||||
new FruitCompositionTool(),
|
||||
new BananaShowerCompositionTool()
|
||||
};
|
||||
|
||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
||||
// TODO: implement position snap
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class CatchSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
|
||||
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; }
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
||||
{
|
||||
var blueprint = moveEvent.Blueprint;
|
||||
Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint);
|
||||
Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
|
||||
float deltaX = targetPosition.X - originalPosition.X;
|
||||
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
if (!(h is CatchHitObject hitObject)) return;
|
||||
|
||||
if (hitObject is BananaShower) return;
|
||||
|
||||
// TODO: confine in bounds
|
||||
hitObject.OriginalXBindable.Value += 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;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class DrawableCatchEditorRuleset : DrawableCatchRuleset
|
||||
{
|
||||
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
}
|
||||
}
|
||||
@@ -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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class FruitCompositionTool : HitObjectCompositionTool
|
||||
{
|
||||
public FruitCompositionTool()
|
||||
: base(nameof(Fruit))
|
||||
{
|
||||
}
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,12 @@ using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModDifficultyAdjust : ModDifficultyAdjust
|
||||
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
|
||||
@@ -31,6 +32,9 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
Value = 5,
|
||||
};
|
||||
|
||||
[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);
|
||||
@@ -45,12 +49,14 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
|
||||
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
|
||||
string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
|
||||
|
||||
return string.Join(", ", new[]
|
||||
{
|
||||
circleSize,
|
||||
base.SettingDescription,
|
||||
approachRate
|
||||
approachRate,
|
||||
spicyPatterns,
|
||||
}.Where(s => !string.IsNullOrEmpty(s)));
|
||||
}
|
||||
}
|
||||
@@ -70,5 +76,11 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
|
||||
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
|
||||
}
|
||||
|
||||
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
||||
{
|
||||
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
||||
catchProcessor.HardRockOffsets = HardRockOffsets.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,14 @@ using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
|
||||
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
|
||||
{
|
||||
public override double ScoreMultiplier => 1.12;
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
|
||||
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
||||
{
|
||||
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
||||
catchProcessor.HardRockOffsets = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
/// </summary>
|
||||
private bool providesComboCounter => this.HasFont(LegacyFont.Combo);
|
||||
|
||||
public CatchLegacySkinTransformer(ISkinSource source)
|
||||
: base(source)
|
||||
public CatchLegacySkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
switch (targetComponent.Target)
|
||||
{
|
||||
case SkinnableTarget.MainHUDComponents:
|
||||
var components = Source.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
|
||||
var components = base.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
|
||||
|
||||
if (providesComboCounter && components != null)
|
||||
{
|
||||
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
case CatchSkinComponents.Catcher:
|
||||
var version = Source.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
var version = GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||
|
||||
if (version < 2.3m)
|
||||
{
|
||||
@@ -83,13 +83,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
case CatchSkinComponents.CatchComboCounter:
|
||||
if (providesComboCounter)
|
||||
return new LegacyCatchComboCounter(Source);
|
||||
return new LegacyCatchComboCounter(Skin);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return Source.GetDrawableComponent(component);
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
switch (lookup)
|
||||
{
|
||||
case CatchSkinColour colour:
|
||||
var result = (Bindable<Color4>)Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
var result = (Bindable<Color4>)base.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
if (result == null)
|
||||
return null;
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
return (IBindable<TValue>)result;
|
||||
}
|
||||
|
||||
return Source.GetConfig<TLookup, TValue>(lookup);
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
@@ -28,20 +26,18 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
public const float CENTER_X = WIDTH / 2;
|
||||
|
||||
[Cached]
|
||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
internal readonly CatcherArea CatcherArea;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
// only check the X position; handle all vertical space.
|
||||
base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y));
|
||||
|
||||
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation)
|
||||
public CatchPlayfield(BeatmapDifficulty difficulty)
|
||||
{
|
||||
var droppedObjectContainer = new Container<CaughtObject>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
CatcherArea = new CatcherArea(droppedObjectContainer, difficulty)
|
||||
CatcherArea = new CatcherArea(difficulty)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
@@ -49,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
droppedObjectContainer,
|
||||
droppedObjectContainer = new DroppedObjectContainer(),
|
||||
CatcherArea.MovableCatcher.CreateProxiedContent(),
|
||||
HitObjectContainer.CreateProxy(),
|
||||
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
|
||||
|
||||
@@ -79,7 +79,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <summary>
|
||||
/// Contains objects dropped from the plate.
|
||||
/// </summary>
|
||||
private readonly Container<CaughtObject> droppedObjectTarget;
|
||||
[Resolved]
|
||||
private DroppedObjectContainer droppedObjectTarget { get; set; }
|
||||
|
||||
public CatcherAnimationState CurrentState
|
||||
{
|
||||
@@ -134,10 +135,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
|
||||
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
|
||||
|
||||
public Catcher([NotNull] Container trailsTarget, [NotNull] Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty = null)
|
||||
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
this.trailsTarget = trailsTarget;
|
||||
this.droppedObjectTarget = droppedObjectTarget;
|
||||
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private int currentDirection;
|
||||
|
||||
public CatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty difficulty = null)
|
||||
public CatcherArea(BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
|
||||
Children = new Drawable[]
|
||||
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
Margin = new MarginPadding { Bottom = 350f },
|
||||
X = CatchPlayfield.CENTER_X
|
||||
},
|
||||
MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X },
|
||||
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
protected override void FreeAfterUse()
|
||||
{
|
||||
ClearTransforms();
|
||||
Alpha = 1;
|
||||
base.FreeAfterUse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
|
||||
|
||||
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation);
|
||||
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class DroppedObjectContainer : Container<CaughtObject>
|
||||
{
|
||||
public DroppedObjectContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap);
|
||||
|
||||
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
@@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
private class TimeSlider : OsuSliderBar<double>
|
||||
{
|
||||
public override string TooltipText => Current.Value.ToString("N0") + "ms";
|
||||
public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,29 +50,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{ HitResult.Miss, "mania-hit0" }
|
||||
};
|
||||
|
||||
private Lazy<bool> isLegacySkin;
|
||||
private readonly Lazy<bool> isLegacySkin;
|
||||
|
||||
/// <summary>
|
||||
/// Whether texture for the keys exists.
|
||||
/// Used to determine if the mania ruleset is skinned.
|
||||
/// </summary>
|
||||
private Lazy<bool> hasKeyTexture;
|
||||
private readonly Lazy<bool> hasKeyTexture;
|
||||
|
||||
public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap)
|
||||
: base(source)
|
||||
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
: base(skin)
|
||||
{
|
||||
this.beatmap = (ManiaBeatmap)beatmap;
|
||||
|
||||
Source.SourceChanged += sourceChanged;
|
||||
sourceChanged();
|
||||
}
|
||||
|
||||
private void sourceChanged()
|
||||
{
|
||||
isLegacySkin = new Lazy<bool>(() => FindProvider(s => s.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() => FindProvider(s => s.GetAnimation(
|
||||
s.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
|
||||
?? "mania-key1", true, true) != null) != null);
|
||||
isLegacySkin = new Lazy<bool>(() => GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() =>
|
||||
{
|
||||
var keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
|
||||
return this.GetAnimation(keyImage, true, true) != null;
|
||||
});
|
||||
}
|
||||
|
||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||
@@ -125,7 +121,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
break;
|
||||
}
|
||||
|
||||
return Source.GetDrawableComponent(component);
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
private Drawable getResult(HitResult result)
|
||||
@@ -146,15 +142,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
|
||||
return new SampleVirtual();
|
||||
|
||||
return Source.GetSample(sampleInfo);
|
||||
return base.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
|
||||
return Source.GetConfig<LegacyManiaSkinConfigurationLookup, TValue>(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
|
||||
return base.GetConfig<LegacyManiaSkinConfigurationLookup, TValue>(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
|
||||
|
||||
return Source.GetConfig<TLookup, TValue>(lookup);
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Checks;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckLowDiffOverlapsTest
|
||||
{
|
||||
private CheckLowDiffOverlaps check;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckLowDiffOverlaps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoOverlapFarApart()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(200, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoOverlapClose()
|
||||
{
|
||||
assertShouldProbablyOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 167, Position = new Vector2(200, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoOverlapTooClose()
|
||||
{
|
||||
assertShouldOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(200, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoOverlapTooCloseExpert()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(200, 0) }
|
||||
}
|
||||
}, DifficultyRating.Expert);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlapClose()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 167, Position = new Vector2(20, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlapFarApart()
|
||||
{
|
||||
assertShouldNotOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(20, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAlmostOverlapFarApart()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
// Default circle diameter is 128 px, but part of that is the fade/border of the circle.
|
||||
// We want this to only be a problem when it actually looks like an overlap.
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(125, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAlmostNotOverlapFarApart()
|
||||
{
|
||||
assertShouldNotOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(110, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlapFarApartExpert()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(20, 0) }
|
||||
}
|
||||
}, DifficultyRating.Expert);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOverlapTooFarApart()
|
||||
{
|
||||
// Far apart enough to where the objects are not visible at the same time, and so overlapping is fine.
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 2000, Position = new Vector2(20, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderTailOverlapFarApart()
|
||||
{
|
||||
assertShouldNotOverlap(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderTailOverlapClose()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderTailNoOverlapFarApart()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderTailNoOverlapClose()
|
||||
{
|
||||
// If these were circles they would need to overlap, but overlapping with slider tails is not required.
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mock<Slider> getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition)
|
||||
{
|
||||
var mockSlider = new Mock<Slider>();
|
||||
mockSlider.SetupGet(s => s.StartTime).Returns(startTime);
|
||||
mockSlider.SetupGet(s => s.Position).Returns(startPosition);
|
||||
mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition);
|
||||
mockSlider.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
|
||||
|
||||
return mockSlider;
|
||||
}
|
||||
|
||||
private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertShouldProbablyOverlap(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldProbablyOverlap));
|
||||
}
|
||||
|
||||
private void assertShouldOverlap(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldOverlap));
|
||||
}
|
||||
|
||||
private void assertShouldNotOverlap(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldNotOverlap));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
// 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.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
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 CheckTimeDistanceEqualityTest
|
||||
{
|
||||
private CheckTimeDistanceEquality check;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckTimeDistanceEquality();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesEquidistant()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(100, 0) },
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(150, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesOneSlightlyOff()
|
||||
{
|
||||
assertWarning(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(80, 0) }, // Distance a quite low compared to previous.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(130, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesOneOff()
|
||||
{
|
||||
assertProblem(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesTwoOff()
|
||||
{
|
||||
assertProblem(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(250, 0) } // Also twice the regular spacing.
|
||||
}
|
||||
}, count: 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesStacked()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(50, 0) }, // Stacked, is fine.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesStacking()
|
||||
{
|
||||
assertWarning(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(50, 0), StackHeight = 1 },
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(50, 0), StackHeight = 2 },
|
||||
new HitCircle { StartTime = 2000, Position = new Vector2(50, 0), StackHeight = 3 },
|
||||
new HitCircle { StartTime = 2500, Position = new Vector2(50, 0), StackHeight = 4 }, // Ends up far from (50; 0), causing irregular spacing.
|
||||
new HitCircle { StartTime = 3000, Position = new Vector2(100, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesHalfStack()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(55, 0) }, // Basically stacked, so is fine.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(105, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesPartialOverlap()
|
||||
{
|
||||
assertProblem(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(65, 0) }, // Really low distance compared to previous.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(115, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesSlightlyDifferent()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
// Does not need to be perfect, as long as the distance is approximately correct it's sight-readable.
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(52, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(97, 0) },
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(165, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesSlowlyChanging()
|
||||
{
|
||||
const float multiplier = 1.2f;
|
||||
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) },
|
||||
// This gap would be a warning if it weren't for the previous pushing the average spacing up.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesQuicklyChanging()
|
||||
{
|
||||
const float multiplier = 1.6f;
|
||||
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) }, // Warning
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) } // Problem
|
||||
}
|
||||
};
|
||||
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(2));
|
||||
Assert.That(issues.First().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning);
|
||||
Assert.That(issues.Last().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesTooFarApart()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 4000, Position = new Vector2(200, 0) }, // 2 seconds apart from previous, so can start from wherever.
|
||||
new HitCircle { StartTime = 4500, Position = new Vector2(250, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesOneOffExpert()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Jumps are allowed in higher difficulties.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }
|
||||
}
|
||||
}, DifficultyRating.Expert);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSpinner()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
new Spinner { StartTime = 500, EndTime = 1000 }, // Distance to and from the spinner should be ignored. If it isn't this should give a problem.
|
||||
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) },
|
||||
new HitCircle { StartTime = 2000, Position = new Vector2(150, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliders()
|
||||
{
|
||||
assertOk(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object,
|
||||
getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(200, 0), endPosition: new Vector2(250, 0)).Object,
|
||||
new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSlidersOneOff()
|
||||
{
|
||||
assertProblem(new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||
getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object,
|
||||
getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(250, 0), endPosition: new Vector2(300, 0)).Object, // Twice the spacing.
|
||||
new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mock<Slider> getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition)
|
||||
{
|
||||
var mockSlider = new Mock<Slider>();
|
||||
mockSlider.SetupGet(s => s.StartTime).Returns(startTime);
|
||||
mockSlider.SetupGet(s => s.Position).Returns(startPosition);
|
||||
mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition);
|
||||
mockSlider.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
|
||||
|
||||
return mockSlider;
|
||||
}
|
||||
|
||||
private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertWarning(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning));
|
||||
}
|
||||
|
||||
private void assertProblem(IBeatmap beatmap, int count = 1)
|
||||
{
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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;
|
||||
@@ -112,7 +113,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
||||
|
||||
public IEnumerable<ISkin> AllSources => new[] { this };
|
||||
|
||||
public event Action SourceChanged
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -164,9 +165,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
||||
|
||||
public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => default;
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
||||
|
||||
public IEnumerable<ISkin> AllSources => new[] { this };
|
||||
|
||||
public event Action SourceChanged;
|
||||
|
||||
|
||||
@@ -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}", () => MusicController.SeekTo(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
|
||||
{
|
||||
|
||||
@@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private void addSeekStep(double time)
|
||||
{
|
||||
AddStep($"seek to {time}", () => MusicController.SeekTo(time));
|
||||
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
|
||||
|
||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<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">
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -283,6 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
}
|
||||
|
||||
public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
|
||||
public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
{
|
||||
public class CheckLowDiffOverlaps : ICheck
|
||||
{
|
||||
// For the lowest difficulties, the osu! Ranking Criteria encourages overlapping ~180 BPM 1/2, but discourages ~180 BPM 1/1.
|
||||
private const double should_overlap_threshold = 150; // 200 BPM 1/2
|
||||
private const double should_probably_overlap_threshold = 175; // 170 BPM 1/2
|
||||
private const double should_not_overlap_threshold = 250; // 120 BPM 1/2 = 240 BPM 1/1
|
||||
|
||||
/// <summary>
|
||||
/// Objects need to overlap this much before being treated as an overlap, else it may just be the borders slightly touching.
|
||||
/// </summary>
|
||||
private const double overlap_leniency = 5;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Missing or unexpected overlaps");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateShouldOverlap(this),
|
||||
new IssueTemplateShouldProbablyOverlap(this),
|
||||
new IssueTemplateShouldNotOverlap(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
// TODO: This should also apply to *lowest difficulty* Normals - they are skipped for now.
|
||||
if (context.InterpretedDifficulty > DifficultyRating.Easy)
|
||||
yield break;
|
||||
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
{
|
||||
if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner)
|
||||
continue;
|
||||
|
||||
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
|
||||
continue;
|
||||
|
||||
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||
if (deltaTime >= hitObject.TimeFadeIn + hitObject.TimePreempt)
|
||||
// The objects are not visible at the same time (without mods), hence skipping.
|
||||
continue;
|
||||
|
||||
var distanceSq = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthSquared;
|
||||
var diameter = (hitObject.Radius - overlap_leniency) * 2;
|
||||
var diameterSq = diameter * diameter;
|
||||
|
||||
bool areOverlapping = distanceSq < diameterSq;
|
||||
|
||||
// Slider ends do not need to be overlapped because of slider leniency.
|
||||
if (!areOverlapping && !(hitObject is Slider))
|
||||
{
|
||||
if (deltaTime < should_overlap_threshold)
|
||||
yield return new IssueTemplateShouldOverlap(this).Create(deltaTime, hitObject, nextHitObject);
|
||||
else if (deltaTime < should_probably_overlap_threshold)
|
||||
yield return new IssueTemplateShouldProbablyOverlap(this).Create(deltaTime, hitObject, nextHitObject);
|
||||
}
|
||||
|
||||
if (areOverlapping && deltaTime > should_not_overlap_threshold)
|
||||
yield return new IssueTemplateShouldNotOverlap(this).Create(deltaTime, hitObject, nextHitObject);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class IssueTemplateOverlap : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateOverlap(ICheck check, IssueType issueType, string unformattedMessage)
|
||||
: base(check, issueType, unformattedMessage)
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(double deltaTime, params HitObject[] hitObjects) => new Issue(hitObjects, this, deltaTime);
|
||||
}
|
||||
|
||||
public class IssueTemplateShouldOverlap : IssueTemplateOverlap
|
||||
{
|
||||
public IssueTemplateShouldOverlap(ICheck check)
|
||||
: base(check, IssueType.Problem, "These are {0} ms apart and so should be overlapping.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateShouldProbablyOverlap : IssueTemplateOverlap
|
||||
{
|
||||
public IssueTemplateShouldProbablyOverlap(ICheck check)
|
||||
: base(check, IssueType.Warning, "These are {0} ms apart and so should probably be overlapping.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateShouldNotOverlap : IssueTemplateOverlap
|
||||
{
|
||||
public IssueTemplateShouldNotOverlap(ICheck check)
|
||||
: base(check, IssueType.Problem, "These are {0} ms apart and so should NOT be overlapping.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
{
|
||||
public class CheckTimeDistanceEquality : ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Two objects this many ms apart or more are skipped. (200 BPM 2/1)
|
||||
/// </summary>
|
||||
private const double pattern_lifetime = 600;
|
||||
|
||||
/// <summary>
|
||||
/// Two objects this distance apart or less are skipped.
|
||||
/// </summary>
|
||||
private const double stack_leniency = 12;
|
||||
|
||||
/// <summary>
|
||||
/// How long an observation is relevant for comparison. (120 BPM 8/1)
|
||||
/// </summary>
|
||||
private const double observation_lifetime = 4000;
|
||||
|
||||
/// <summary>
|
||||
/// How different two delta times can be to still be compared. (240 BPM 1/16)
|
||||
/// </summary>
|
||||
private const double similar_time_leniency = 16;
|
||||
|
||||
/// <summary>
|
||||
/// How many pixels are subtracted from the difference between current and expected distance.
|
||||
/// </summary>
|
||||
private const double distance_leniency_absolute_warning = 10;
|
||||
|
||||
/// <summary>
|
||||
/// How much of the current distance that the difference can make out.
|
||||
/// </summary>
|
||||
private const double distance_leniency_percent_warning = 0.15;
|
||||
|
||||
private const double distance_leniency_absolute_problem = 20;
|
||||
private const double distance_leniency_percent_problem = 0.3;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Object too close or far away from previous");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateIrregularSpacingProblem(this),
|
||||
new IssueTemplateIrregularSpacingWarning(this)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Represents an observation of the time and distance between two objects.
|
||||
/// </summary>
|
||||
private readonly struct ObservedTimeDistance
|
||||
{
|
||||
public readonly double ObservationTime;
|
||||
public readonly double DeltaTime;
|
||||
public readonly double Distance;
|
||||
|
||||
public ObservedTimeDistance(double observationTime, double deltaTime, double distance)
|
||||
{
|
||||
ObservationTime = observationTime;
|
||||
DeltaTime = deltaTime;
|
||||
Distance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
if (context.InterpretedDifficulty > DifficultyRating.Normal)
|
||||
yield break;
|
||||
|
||||
var prevObservedTimeDistances = new List<ObservedTimeDistance>();
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
{
|
||||
if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner)
|
||||
continue;
|
||||
|
||||
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
|
||||
continue;
|
||||
|
||||
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||
|
||||
// Ignore objects that are far enough apart in time to not be considered the same pattern.
|
||||
if (deltaTime > pattern_lifetime)
|
||||
continue;
|
||||
|
||||
// Relying on FastInvSqrt is probably good enough here. We'll be taking the difference between distances later, hence square not being sufficient.
|
||||
var distance = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthFast;
|
||||
|
||||
// Ignore stacks and half-stacks, as these are close enough to where they can't be confused for being time-distanced.
|
||||
if (distance < stack_leniency)
|
||||
continue;
|
||||
|
||||
var observedTimeDistance = new ObservedTimeDistance(nextHitObject.StartTime, deltaTime, distance);
|
||||
var expectedDistance = getExpectedDistance(prevObservedTimeDistances, observedTimeDistance);
|
||||
|
||||
if (expectedDistance == 0)
|
||||
{
|
||||
// There was nothing relevant to compare to.
|
||||
prevObservedTimeDistances.Add(observedTimeDistance);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_problem) / distance > distance_leniency_percent_problem)
|
||||
yield return new IssueTemplateIrregularSpacingProblem(this).Create(expectedDistance, distance, hitObject, nextHitObject);
|
||||
else if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_warning) / distance > distance_leniency_percent_warning)
|
||||
yield return new IssueTemplateIrregularSpacingWarning(this).Create(expectedDistance, distance, hitObject, nextHitObject);
|
||||
else
|
||||
{
|
||||
// We use `else` here to prevent issues from cascading; an object spaced too far could cause regular spacing to be considered "too short" otherwise.
|
||||
prevObservedTimeDistances.Add(observedTimeDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double getExpectedDistance(IEnumerable<ObservedTimeDistance> prevObservedTimeDistances, ObservedTimeDistance observedTimeDistance)
|
||||
{
|
||||
var observations = prevObservedTimeDistances.Count();
|
||||
|
||||
int count = 0;
|
||||
double sum = 0;
|
||||
|
||||
// Looping this in reverse allows us to break before going through all elements, as we're only interested in the most recent ones.
|
||||
for (int i = observations - 1; i >= 0; --i)
|
||||
{
|
||||
var prevObservedTimeDistance = prevObservedTimeDistances.ElementAt(i);
|
||||
|
||||
// Only consider observations within the last few seconds - this allows the map to build spacing up/down over time, but prevents it from being too sudden.
|
||||
if (observedTimeDistance.ObservationTime - prevObservedTimeDistance.ObservationTime > observation_lifetime)
|
||||
break;
|
||||
|
||||
// Only consider observations which have a similar time difference - this leniency allows handling of multi-BPM maps which speed up/down slowly.
|
||||
if (Math.Abs(observedTimeDistance.DeltaTime - prevObservedTimeDistance.DeltaTime) > similar_time_leniency)
|
||||
break;
|
||||
|
||||
count += 1;
|
||||
sum += prevObservedTimeDistance.Distance / Math.Max(prevObservedTimeDistance.DeltaTime, 1);
|
||||
}
|
||||
|
||||
return sum / Math.Max(count, 1) * observedTimeDistance.DeltaTime;
|
||||
}
|
||||
|
||||
public abstract class IssueTemplateIrregularSpacing : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateIrregularSpacing(ICheck check, IssueType issueType)
|
||||
: base(check, issueType, "Expected {0:0} px spacing like previous objects, currently {1:0}.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(double expected, double actual, params HitObject[] hitObjects) => new Issue(hitObjects, this, expected, actual);
|
||||
}
|
||||
|
||||
public class IssueTemplateIrregularSpacingProblem : IssueTemplateIrregularSpacing
|
||||
{
|
||||
public IssueTemplateIrregularSpacingProblem(ICheck check)
|
||||
: base(check, IssueType.Problem)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateIrregularSpacingWarning : IssueTemplateIrregularSpacing
|
||||
{
|
||||
public IssueTemplateIrregularSpacingWarning(ICheck check)
|
||||
: base(check, IssueType.Warning)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
new CheckOffscreenObjects()
|
||||
// Compose
|
||||
new CheckOffscreenObjects(),
|
||||
|
||||
// Spread
|
||||
new CheckTimeDistanceEquality(),
|
||||
new CheckLowDiffOverlaps()
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -11,15 +11,16 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
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;
|
||||
@@ -110,6 +111,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// hide elements we don't care about.
|
||||
// todo: hide background
|
||||
|
||||
spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner);
|
||||
hideSpinnerApproachCircle(spinner);
|
||||
|
||||
using (spinner.BeginAbsoluteSequence(fadeStartTime))
|
||||
spinner.FadeOut(fadeDuration);
|
||||
|
||||
@@ -160,5 +164,15 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void hideSpinnerApproachCircle(DrawableSpinner spinner)
|
||||
{
|
||||
var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle;
|
||||
if (approachCircle == null)
|
||||
return;
|
||||
|
||||
using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt))
|
||||
approachCircle.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Osu.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
@@ -23,15 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override string Description => "It never gets boring!";
|
||||
|
||||
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
|
||||
// The closer the hit objects draw to the border, the sharper the turn
|
||||
private const float playfield_edge_ratio = 0.375f;
|
||||
|
||||
private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
|
||||
private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
|
||||
|
||||
private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
|
||||
|
||||
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
|
||||
|
||||
private Random rng;
|
||||
@@ -113,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
distanceToPrev * (float)Math.Sin(current.AngleRad)
|
||||
);
|
||||
|
||||
posRelativeToPrev = getRotatedVector(previous.EndPositionRandomised, posRelativeToPrev);
|
||||
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev);
|
||||
|
||||
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
|
||||
|
||||
@@ -185,73 +177,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the position of the current hit object relative to the previous one.
|
||||
/// </summary>
|
||||
/// <returns>The position of the current hit object relative to the previous one</returns>
|
||||
private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev)
|
||||
{
|
||||
var relativeRotationDistance = 0f;
|
||||
|
||||
if (prevPosChanged.X < playfield_middle.X)
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(border_distance_x - prevPosChanged.X) / border_distance_x,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
|
||||
if (prevPosChanged.Y < playfield_middle.Y)
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(border_distance_y - prevPosChanged.Y) / border_distance_y,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
|
||||
return rotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevPosChanged, relativeRotationDistance / 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates vector "initial" towards vector "destinantion"
|
||||
/// </summary>
|
||||
/// <param name="initial">Vector to rotate to "destination"</param>
|
||||
/// <param name="destination">Vector "initial" should be rotated to</param>
|
||||
/// <param name="relativeDistance">The angle the vector should be rotated relative to the difference between the angles of the the two vectors.</param>
|
||||
/// <returns>Resulting vector</returns>
|
||||
private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance)
|
||||
{
|
||||
var initialAngleRad = Math.Atan2(initial.Y, initial.X);
|
||||
var destAngleRad = Math.Atan2(destination.Y, destination.X);
|
||||
|
||||
var diff = destAngleRad - initialAngleRad;
|
||||
|
||||
while (diff < -Math.PI) diff += 2 * Math.PI;
|
||||
|
||||
while (diff > Math.PI) diff -= 2 * Math.PI;
|
||||
|
||||
var finalAngleRad = initialAngleRad + relativeDistance * diff;
|
||||
|
||||
return new Vector2(
|
||||
initial.Length * (float)Math.Cos(finalAngleRad),
|
||||
initial.Length * (float)Math.Sin(finalAngleRad)
|
||||
);
|
||||
}
|
||||
|
||||
private class RandomObjectInfo
|
||||
{
|
||||
public float AngleRad { get; set; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
@@ -19,7 +20,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece
|
||||
public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece, IHasApproachCircle
|
||||
{
|
||||
public OsuAction? HitAction => HitArea.HitAction;
|
||||
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
|
||||
@@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public HitReceptor HitArea { get; private set; }
|
||||
public SkinnableDrawable CirclePiece { get; private set; }
|
||||
|
||||
Drawable IHasApproachCircle.ApproachCircle => ApproachCircle;
|
||||
|
||||
private Container scaleContainer;
|
||||
private InputManager inputManager;
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
||||
|
||||
public SkinnableDrawable Body { get; private set; }
|
||||
|
||||
public SpinnerRotationTracker RotationTracker { get; private set; }
|
||||
|
||||
private SpinnerSpmCalculator spmCalculator;
|
||||
@@ -86,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
|
||||
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
|
||||
RotationTracker = new SpinnerRotationTracker(this)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new OsuLegacySkinTransformer(source);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new OsuLegacySkinTransformer(skin);
|
||||
|
||||
public int LegacyID => 0;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Osu
|
||||
Cursor,
|
||||
CursorTrail,
|
||||
SliderScorePoint,
|
||||
ApproachCircle,
|
||||
ReverseArrow,
|
||||
HitCircleText,
|
||||
SliderHeadHitCircle,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A common interface between implementations which provide an approach circle.
|
||||
/// </summary>
|
||||
public interface IHasApproachCircle
|
||||
{
|
||||
/// <summary>
|
||||
/// The approach circle drawable.
|
||||
/// </summary>
|
||||
Drawable ApproachCircle { get; }
|
||||
}
|
||||
}
|
||||
@@ -55,28 +55,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-bottom")
|
||||
Texture = source.GetTexture("spinner-bottom"),
|
||||
},
|
||||
discTop = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-top")
|
||||
Texture = source.GetTexture("spinner-top"),
|
||||
},
|
||||
fixedMiddle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-middle")
|
||||
Texture = source.GetTexture("spinner-middle"),
|
||||
},
|
||||
spinningMiddle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-middle2")
|
||||
}
|
||||
Texture = source.GetTexture("spinner-middle2"),
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin))
|
||||
{
|
||||
AddInternal(ApproachCircle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-approachcircle"),
|
||||
Scale = new Vector2(SPRITE_SCALE * 1.86f),
|
||||
Y = SPINNER_Y_CENTRE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
spinnerBlink = source.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
new Sprite
|
||||
{
|
||||
@@ -68,6 +68,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Origin = Anchor.TopLeft,
|
||||
Scale = new Vector2(SPRITE_SCALE)
|
||||
}
|
||||
},
|
||||
ApproachCircle = new Sprite
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-approachcircle"),
|
||||
Scale = new Vector2(SPRITE_SCALE * 1.86f),
|
||||
Y = SPINNER_Y_CENTRE,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public abstract class LegacySpinner : CompositeDrawable
|
||||
public abstract class LegacySpinner : CompositeDrawable, IHasApproachCircle
|
||||
{
|
||||
public const float SPRITE_SCALE = 0.625f;
|
||||
|
||||
/// <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.
|
||||
@@ -26,12 +28,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
|
||||
|
||||
protected const float SPRITE_SCALE = 0.625f;
|
||||
|
||||
private const float spm_hide_offset = 50f;
|
||||
|
||||
protected DrawableSpinner DrawableSpinner { get; private set; }
|
||||
|
||||
public Drawable ApproachCircle { get; protected set; }
|
||||
|
||||
private Sprite spin;
|
||||
private Sprite clear;
|
||||
|
||||
@@ -175,6 +177,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
|
||||
}
|
||||
|
||||
using (BeginAbsoluteSequence(d.HitObject.StartTime))
|
||||
ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration);
|
||||
|
||||
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
|
||||
|
||||
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public class OsuLegacySkinTransformer : LegacySkinTransformer
|
||||
{
|
||||
private Lazy<bool> hasHitCircle;
|
||||
private readonly Lazy<bool> hasHitCircle;
|
||||
|
||||
/// <summary>
|
||||
/// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
|
||||
@@ -20,16 +20,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
/// </summary>
|
||||
public const float LEGACY_CIRCLE_RADIUS = 64 - 5;
|
||||
|
||||
public OsuLegacySkinTransformer(ISkinSource source)
|
||||
: base(source)
|
||||
public OsuLegacySkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
Source.SourceChanged += sourceChanged;
|
||||
sourceChanged();
|
||||
}
|
||||
|
||||
private void sourceChanged()
|
||||
{
|
||||
hasHitCircle = new Lazy<bool>(() => FindProvider(s => s.GetTexture("hitcircle") != null) != null);
|
||||
hasHitCircle = new Lazy<bool>(() => GetTexture("hitcircle") != null);
|
||||
}
|
||||
|
||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||
@@ -49,16 +43,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
return followCircle;
|
||||
|
||||
case OsuSkinComponents.SliderBall:
|
||||
// specular and nd layers must come from the same source as the ball texure.
|
||||
var ballProvider = Source.FindProvider(s => s.GetTexture("sliderb") != null || s.GetTexture("sliderb0") != null);
|
||||
|
||||
var sliderBallContent = ballProvider.GetAnimation("sliderb", true, true, animationSeparator: "");
|
||||
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
|
||||
|
||||
// todo: slider ball has a custom frame delay based on velocity
|
||||
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
|
||||
|
||||
if (sliderBallContent != null)
|
||||
return new LegacySliderBall(sliderBallContent, ballProvider);
|
||||
return new LegacySliderBall(sliderBallContent, this);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -87,18 +78,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.Cursor:
|
||||
var cursorProvider = Source.FindProvider(s => s.GetTexture("cursor") != null);
|
||||
|
||||
if (cursorProvider != null)
|
||||
return new LegacyCursor(cursorProvider);
|
||||
if (GetTexture("cursor") != null)
|
||||
return new LegacyCursor(this);
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.CursorTrail:
|
||||
var trailProvider = Source.FindProvider(s => s.GetTexture("cursortrail") != null);
|
||||
|
||||
if (trailProvider != null)
|
||||
return new LegacyCursorTrail(trailProvider);
|
||||
if (GetTexture("cursortrail") != null)
|
||||
return new LegacyCursorTrail(this);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -113,9 +100,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
};
|
||||
|
||||
case OsuSkinComponents.SpinnerBody:
|
||||
bool hasBackground = Source.GetTexture("spinner-background") != null;
|
||||
bool hasBackground = GetTexture("spinner-background") != null;
|
||||
|
||||
if (Source.GetTexture("spinner-top") != null && !hasBackground)
|
||||
if (GetTexture("spinner-top") != null && !hasBackground)
|
||||
return new LegacyNewStyleSpinner();
|
||||
else if (hasBackground)
|
||||
return new LegacyOldStyleSpinner();
|
||||
@@ -124,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
}
|
||||
}
|
||||
|
||||
return Source.GetDrawableComponent(component);
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
@@ -132,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
switch (lookup)
|
||||
{
|
||||
case OsuSkinColour colour:
|
||||
return Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
return base.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
|
||||
|
||||
case OsuSkinConfiguration osuLookup:
|
||||
switch (osuLookup)
|
||||
@@ -146,14 +133,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
case OsuSkinConfiguration.HitCircleOverlayAboveNumber:
|
||||
// See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D
|
||||
// HitCircleOverlayAboveNumer (with typo) should still be supported for now.
|
||||
return Source.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
|
||||
Source.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
|
||||
return base.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
|
||||
base.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return Source.GetConfig<TLookup, TValue>(lookup);
|
||||
return base.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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.Game.Rulesets.Osu.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Utils
|
||||
{
|
||||
public static class OsuHitObjectGenerationUtils
|
||||
{
|
||||
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
|
||||
// The closer the hit objects draw to the border, the sharper the turn
|
||||
private const float playfield_edge_ratio = 0.375f;
|
||||
|
||||
private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
|
||||
private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
|
||||
|
||||
private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
|
||||
|
||||
/// <summary>
|
||||
/// Rotate a hit object away from the playfield edge, while keeping a constant distance
|
||||
/// from the previous object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The extent of rotation depends on the position of the hit object. Hit objects
|
||||
/// closer to the playfield edge will be rotated to a larger extent.
|
||||
/// </remarks>
|
||||
/// <param name="prevObjectPos">Position of the previous hit object.</param>
|
||||
/// <param name="posRelativeToPrev">Position of the hit object to be rotated, relative to the previous hit object.</param>
|
||||
/// <param name="rotationRatio">
|
||||
/// The extent of rotation.
|
||||
/// 0 means the hit object is never rotated.
|
||||
/// 1 means the hit object will be fully rotated towards playfield center when it is originally at playfield edge.
|
||||
/// </param>
|
||||
/// <returns>The new position of the hit object, relative to the previous one.</returns>
|
||||
public static Vector2 RotateAwayFromEdge(Vector2 prevObjectPos, Vector2 posRelativeToPrev, float rotationRatio = 0.5f)
|
||||
{
|
||||
var relativeRotationDistance = 0f;
|
||||
|
||||
if (prevObjectPos.X < playfield_middle.X)
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(border_distance_x - prevObjectPos.X) / border_distance_x,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(prevObjectPos.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
|
||||
if (prevObjectPos.Y < playfield_middle.Y)
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(border_distance_y - prevObjectPos.Y) / border_distance_y,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(prevObjectPos.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
|
||||
return RotateVectorTowardsVector(
|
||||
posRelativeToPrev,
|
||||
playfield_middle - prevObjectPos,
|
||||
Math.Min(1, relativeRotationDistance * rotationRatio)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates vector "initial" towards vector "destination".
|
||||
/// </summary>
|
||||
/// <param name="initial">The vector to be rotated.</param>
|
||||
/// <param name="destination">The vector that "initial" should be rotated towards.</param>
|
||||
/// <param name="rotationRatio">How much "initial" should be rotated. 0 means no rotation. 1 means "initial" is fully rotated to equal "destination".</param>
|
||||
/// <returns>The rotated vector.</returns>
|
||||
public static Vector2 RotateVectorTowardsVector(Vector2 initial, Vector2 destination, float rotationRatio)
|
||||
{
|
||||
var initialAngleRad = MathF.Atan2(initial.Y, initial.X);
|
||||
var destAngleRad = MathF.Atan2(destination.Y, destination.X);
|
||||
|
||||
var diff = destAngleRad - initialAngleRad;
|
||||
|
||||
while (diff < -MathF.PI) diff += 2 * MathF.PI;
|
||||
|
||||
while (diff > MathF.PI) diff -= 2 * MathF.PI;
|
||||
|
||||
var finalAngleRad = initialAngleRad + rotationRatio * diff;
|
||||
|
||||
return new Vector2(
|
||||
initial.Length * MathF.Cos(finalAngleRad),
|
||||
initial.Length * MathF.Sin(finalAngleRad)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -15,18 +14,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
public class TaikoLegacySkinTransformer : LegacySkinTransformer
|
||||
{
|
||||
private Lazy<bool> hasExplosion;
|
||||
private readonly Lazy<bool> hasExplosion;
|
||||
|
||||
public TaikoLegacySkinTransformer(ISkinSource source)
|
||||
: base(source)
|
||||
public TaikoLegacySkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
Source.SourceChanged += sourceChanged;
|
||||
sourceChanged();
|
||||
}
|
||||
|
||||
private void sourceChanged()
|
||||
{
|
||||
hasExplosion = new Lazy<bool>(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
|
||||
hasExplosion = new Lazy<bool>(() => GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
|
||||
}
|
||||
|
||||
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||
@@ -56,7 +49,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
|
||||
case TaikoSkinComponents.CentreHit:
|
||||
case TaikoSkinComponents.RimHit:
|
||||
|
||||
if (GetTexture("taikohitcircle") != null)
|
||||
return new LegacyHit(taikoComponent.Component);
|
||||
|
||||
@@ -91,7 +83,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionMiss:
|
||||
|
||||
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
|
||||
if (missSprite != null)
|
||||
return new LegacyHitExplosion(missSprite);
|
||||
@@ -100,7 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
|
||||
case TaikoSkinComponents.TaikoExplosionOk:
|
||||
case TaikoSkinComponents.TaikoExplosionGreat:
|
||||
|
||||
var hitName = getHitName(taikoComponent.Component);
|
||||
var hitSprite = this.GetAnimation(hitName, true, false);
|
||||
|
||||
@@ -132,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
}
|
||||
}
|
||||
|
||||
return Source.GetDrawableComponent(component);
|
||||
return base.GetDrawableComponent(component);
|
||||
}
|
||||
|
||||
private string getHitName(TaikoSkinComponents component)
|
||||
@@ -155,13 +145,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
public override ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
if (sampleInfo is HitSampleInfo hitSampleInfo)
|
||||
return Source.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
|
||||
return base.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
|
||||
|
||||
return base.GetSample(sampleInfo);
|
||||
}
|
||||
|
||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Source.GetConfig<TLookup, TValue>(lookup);
|
||||
|
||||
private class LegacyTaikoSampleInfo : HitSampleInfo
|
||||
{
|
||||
public LegacyTaikoSampleInfo(HitSampleInfo sampleInfo)
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this);
|
||||
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TaikoLegacySkinTransformer(source);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TaikoLegacySkinTransformer(skin);
|
||||
|
||||
public const string SHORT_NAME = "taiko";
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Scores.IO;
|
||||
using osu.Game.Users;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Zip;
|
||||
@@ -185,13 +187,62 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
}
|
||||
}
|
||||
|
||||
private string hashFile(string filename)
|
||||
[Test]
|
||||
public async Task TestImportThenImportWithChangedHashedFile()
|
||||
{
|
||||
using (var s = File.OpenRead(filename))
|
||||
return s.ComputeMD5Hash();
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host);
|
||||
|
||||
var temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
Directory.CreateDirectory(extractedFolder);
|
||||
|
||||
try
|
||||
{
|
||||
var imported = await LoadOszIntoOsu(osu);
|
||||
|
||||
await createScoreForBeatmap(osu, imported.Beatmaps.First());
|
||||
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(extractedFolder);
|
||||
|
||||
// arbitrary write to hashed file
|
||||
// this triggers the special BeatmapManager.PreImport deletion/replacement flow.
|
||||
using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText())
|
||||
await sw.WriteLineAsync("// changed");
|
||||
|
||||
using (var zip = ZipArchive.Create())
|
||||
{
|
||||
zip.AddAllFromDirectory(extractedFolder);
|
||||
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
|
||||
}
|
||||
|
||||
var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
|
||||
|
||||
ensureLoaded(osu);
|
||||
|
||||
// check the newly "imported" beatmap is not the original.
|
||||
Assert.IsTrue(imported.ID != importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("intentionally broken by import optimisations")]
|
||||
public async Task TestImportThenImportWithChangedFile()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
@@ -294,6 +345,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("intentionally broken by import optimisations")]
|
||||
public async Task TestImportCorruptThenImport()
|
||||
{
|
||||
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
|
||||
@@ -439,12 +491,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
|
||||
[Test]
|
||||
public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing()
|
||||
{
|
||||
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}"))
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}"))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -452,10 +503,8 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
var imported = await LoadOszIntoOsu(osu);
|
||||
|
||||
if (set)
|
||||
imported.OnlineBeatmapSetID = 1234;
|
||||
else
|
||||
imported.Beatmaps.First().OnlineBeatmapID = 1234;
|
||||
foreach (var b in imported.Beatmaps)
|
||||
b.OnlineBeatmapID = null;
|
||||
|
||||
osu.Dependencies.Get<BeatmapManager>().Update(imported);
|
||||
|
||||
@@ -895,7 +944,17 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending);
|
||||
}
|
||||
|
||||
private void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
|
||||
private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap)
|
||||
{
|
||||
return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
|
||||
{
|
||||
OnlineScoreID = 2,
|
||||
Beatmap = beatmap,
|
||||
BeatmapInfoID = beatmap.ID
|
||||
}, new ImportScoreTest.TestArchiveReader());
|
||||
}
|
||||
|
||||
private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
|
||||
{
|
||||
var manager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
@@ -904,12 +963,18 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
: manager.GetAllUsableBeatmapSets().Count);
|
||||
}
|
||||
|
||||
private void checkBeatmapCount(OsuGameBase osu, int expected)
|
||||
private static string hashFile(string filename)
|
||||
{
|
||||
using (var s = File.OpenRead(filename))
|
||||
return s.ComputeMD5Hash();
|
||||
}
|
||||
|
||||
private static void checkBeatmapCount(OsuGameBase osu, int expected)
|
||||
{
|
||||
Assert.AreEqual(expected, osu.Dependencies.Get<BeatmapManager>().QueryBeatmaps(_ => true).ToList().Count);
|
||||
}
|
||||
|
||||
private void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
|
||||
private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
|
||||
{
|
||||
Assert.AreEqual(expected, osu.Dependencies.Get<FileStore>().QueryFiles(f => f.ReferenceCount == 1).Count());
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace osu.Game.Tests.Chat
|
||||
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")]
|
||||
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")]
|
||||
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/abc", "https://dev.ppy.sh/beatmapsets/abc")]
|
||||
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions", "https://dev.ppy.sh/beatmapsets/discussions")]
|
||||
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions/123", "https://dev.ppy.sh/beatmapsets/discussions/123")]
|
||||
public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link)
|
||||
{
|
||||
MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Chat
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneChannelManager : OsuTestScene
|
||||
{
|
||||
private ChannelManager channelManager;
|
||||
private int currentMessageId;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
var container = new ChannelManagerContainer();
|
||||
Child = container;
|
||||
channelManager = container.ChannelManager;
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("register request handling", () =>
|
||||
{
|
||||
currentMessageId = 0;
|
||||
|
||||
((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case JoinChannelRequest joinChannel:
|
||||
joinChannel.TriggerSuccess();
|
||||
return true;
|
||||
|
||||
case PostMessageRequest postMessage:
|
||||
postMessage.TriggerSuccess(new Message(++currentMessageId)
|
||||
{
|
||||
IsAction = postMessage.Message.IsAction,
|
||||
ChannelId = postMessage.Message.ChannelId,
|
||||
Content = postMessage.Message.Content,
|
||||
Links = postMessage.Message.Links,
|
||||
Timestamp = postMessage.Message.Timestamp,
|
||||
Sender = postMessage.Message.Sender
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommandsPostedToCorrectChannelWhenNotCurrent()
|
||||
{
|
||||
Channel channel1 = null;
|
||||
Channel channel2 = null;
|
||||
|
||||
AddStep("join 2 rooms", () =>
|
||||
{
|
||||
channelManager.JoinChannel(channel1 = createChannel(1, ChannelType.Public));
|
||||
channelManager.JoinChannel(channel2 = createChannel(2, ChannelType.Public));
|
||||
});
|
||||
|
||||
AddStep("select channel 1", () => channelManager.CurrentChannel.Value = channel1);
|
||||
|
||||
AddStep("post /me command to channel 2", () => channelManager.PostCommand("me dances", channel2));
|
||||
AddAssert("/me command received by channel 2", () => channel2.Messages.Last().Content == "dances");
|
||||
|
||||
AddStep("post /np command to channel 2", () => channelManager.PostCommand("np", channel2));
|
||||
AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to"));
|
||||
}
|
||||
|
||||
private Channel createChannel(int id, ChannelType type) => new Channel(new User())
|
||||
{
|
||||
Id = id,
|
||||
Name = $"Channel {id}",
|
||||
Topic = $"Topic of channel {id} with type {type}",
|
||||
Type = type,
|
||||
};
|
||||
|
||||
private class ChannelManagerContainer : CompositeDrawable
|
||||
{
|
||||
[Cached]
|
||||
public ChannelManager ChannelManager { get; } = new ChannelManager();
|
||||
|
||||
public ChannelManagerContainer()
|
||||
{
|
||||
InternalChild = ChannelManager;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestRealmKeyBindingStore
|
||||
{
|
||||
private NativeStorage storage;
|
||||
|
||||
private RealmKeyBindingStore keyBindingStore;
|
||||
|
||||
private RealmContextFactory realmContextFactory;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
|
||||
|
||||
storage = new NativeStorage(directory.FullName);
|
||||
|
||||
realmContextFactory = new RealmContextFactory(storage);
|
||||
keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefaultsPopulationAndQuery()
|
||||
{
|
||||
Assert.That(query().Count, Is.EqualTo(0));
|
||||
|
||||
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||
|
||||
keyBindingStore.Register(testContainer);
|
||||
|
||||
Assert.That(query().Count, 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));
|
||||
}
|
||||
|
||||
private IQueryable<RealmKeyBinding> query() => realmContextFactory.Context.All<RealmKeyBinding>();
|
||||
|
||||
[Test]
|
||||
public void TestUpdateViaQueriedReference()
|
||||
{
|
||||
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||
|
||||
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())
|
||||
{
|
||||
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 = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
realmContextFactory.Dispose();
|
||||
storage.DeleteDirectory(string.Empty);
|
||||
}
|
||||
|
||||
public class TestKeyBindingContainer : KeyBindingContainer
|
||||
{
|
||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings =>
|
||||
new[]
|
||||
{
|
||||
new KeyBinding(InputKey.Escape, GlobalAction.Back),
|
||||
new KeyBinding(InputKey.Enter, GlobalAction.Select),
|
||||
new KeyBinding(InputKey.Space, GlobalAction.Select),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// 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.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckFewHitsoundsTest
|
||||
{
|
||||
private CheckFewHitsounds check;
|
||||
|
||||
private List<HitSampleInfo> notHitsounded;
|
||||
private List<HitSampleInfo> hitsounded;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckFewHitsounds();
|
||||
notHitsounded = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
|
||||
hitsounded = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH)
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
|
||||
|
||||
if ((i + 1) % 2 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
|
||||
if ((i + 1) % 3 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
|
||||
if ((i + 1) % 4 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitsoundedWithBreak()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 32; ++i)
|
||||
{
|
||||
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
|
||||
|
||||
if ((i + 1) % 2 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
|
||||
if ((i + 1) % 3 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
|
||||
if ((i + 1) % 4 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
|
||||
// Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue.
|
||||
if (i > 8 && i < 24)
|
||||
continue;
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLightlyHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var samples = i % 8 == 0 ? hitsounded : notHitsounded;
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
assertLongPeriodNegligible(hitObjects, count: 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRarelyHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded;
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
// Should prompt one warning between 1st and 16th, and another between 16th and 31st.
|
||||
assertLongPeriodWarning(hitObjects, count: 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExtremelyRarelyHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 80; ++i)
|
||||
{
|
||||
var samples = i == 40 ? hitsounded : notHitsounded;
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
// Should prompt one problem between 1st and 41st, and another between 41st and 81st.
|
||||
assertLongPeriodProblem(hitObjects, count: 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 20; ++i)
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded });
|
||||
|
||||
assertNoHitsounds(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNestedObjectsHitsounded()
|
||||
{
|
||||
var ticks = new List<HitObject>();
|
||||
for (int i = 1; i < 16; ++i)
|
||||
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded });
|
||||
|
||||
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
|
||||
{
|
||||
Samples = hitsounded
|
||||
};
|
||||
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
assertOk(new List<HitObject> { nested });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNestedObjectsRarelyHitsounded()
|
||||
{
|
||||
var ticks = new List<HitObject>();
|
||||
for (int i = 1; i < 16; ++i)
|
||||
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded });
|
||||
|
||||
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
|
||||
{
|
||||
Samples = hitsounded
|
||||
};
|
||||
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
assertLongPeriodWarning(new List<HitObject> { nested });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConcurrentObjects()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
var ticks = new List<HitObject>();
|
||||
for (int i = 1; i < 10; ++i)
|
||||
ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded });
|
||||
|
||||
var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000)
|
||||
{
|
||||
Samples = notHitsounded
|
||||
};
|
||||
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
hitObjects.Add(nested);
|
||||
|
||||
for (int i = 1; i <= 6; ++i)
|
||||
hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded });
|
||||
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitObjects)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertLongPeriodProblem(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem));
|
||||
}
|
||||
|
||||
private void assertLongPeriodWarning(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning));
|
||||
}
|
||||
|
||||
private void assertLongPeriodNegligible(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible));
|
||||
}
|
||||
|
||||
private void assertNoHitsounds(List<HitObject> hitObjects)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
|
||||
|
||||
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
// 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.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckMutedObjectsTest
|
||||
{
|
||||
private CheckMutedObjects check;
|
||||
private ControlPointInfo cpi;
|
||||
|
||||
private const int volume_regular = 50;
|
||||
private const int volume_low = 15;
|
||||
private const int volume_muted = 5;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckMutedObjects();
|
||||
|
||||
cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
|
||||
cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
|
||||
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalControlPointVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertOk(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLowControlPointVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertLowVolume(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedControlPointVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalSampleVolume()
|
||||
{
|
||||
// The sample volume should take precedence over the control point volume.
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertOk(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLowSampleVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertLowVolume(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedSampleVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalSampleVolumeSlider()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine.
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertOk(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedSampleVolumeSliderHead()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedSampleVolumeSliderTail()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail.
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMutedPassive(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedControlPointVolumeSliderHead()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 2250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedControlPointVolumeSliderTail()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
// Ends after the 5% control point.
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMutedPassive(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitObjects)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertLowVolume(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive));
|
||||
}
|
||||
|
||||
private void assertMuted(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive));
|
||||
}
|
||||
|
||||
private void assertMutedPassive(List<HitObject> hitObjects)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
ControlPointInfo = cpi,
|
||||
HitObjects = hitObjects
|
||||
};
|
||||
|
||||
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
public sealed class MockNestableHitObject : HitObject, IHasDuration
|
||||
{
|
||||
private readonly IEnumerable<HitObject> toBeNested;
|
||||
|
||||
public MockNestableHitObject(IEnumerable<HitObject> toBeNested, double startTime, double endTime)
|
||||
{
|
||||
this.toBeNested = toBeNested;
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var hitObject in toBeNested)
|
||||
AddNested(hitObject);
|
||||
}
|
||||
|
||||
public double EndTime { get; }
|
||||
|
||||
public double Duration
|
||||
{
|
||||
get => EndTime - StartTime;
|
||||
set => throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,8 @@ namespace osu.Game.Tests
|
||||
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
|
||||
{
|
||||
var osu = new TestOsuGameBase(withBeatmap);
|
||||
Task.Run(() => host.Run(osu));
|
||||
Task.Run(() => host.Run(osu))
|
||||
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
||||
|
||||
|
||||
@@ -14,6 +14,14 @@ namespace osu.Game.Tests.Mods
|
||||
[TestFixture]
|
||||
public class ModUtilsTest
|
||||
{
|
||||
[Test]
|
||||
public void TestModIsNotCompatibleWithItself()
|
||||
{
|
||||
var mod = new Mock<CustomMod1>();
|
||||
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object, mod.Object }, out var invalid), Is.False);
|
||||
Assert.That(invalid, Is.EquivalentTo(new[] { mod.Object }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModIsCompatibleByItself()
|
||||
{
|
||||
@@ -147,7 +155,7 @@ namespace osu.Game.Tests.Mods
|
||||
// multi mod.
|
||||
new object[]
|
||||
{
|
||||
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModHalfTime() },
|
||||
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() },
|
||||
new[] { typeof(MultiMod) }
|
||||
},
|
||||
// valid pair.
|
||||
|
||||
@@ -142,7 +142,10 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
foreach (var file in osuStorage.IgnoreFiles)
|
||||
{
|
||||
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
||||
// avoid touching realm files which may be a pipe and break everything.
|
||||
// this is also done locally inside OsuStorage via the IgnoreFiles list.
|
||||
if (file.EndsWith(".ini", StringComparison.Ordinal))
|
||||
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
||||
Assert.That(storage.Exists(file), Is.False);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,20 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyOverallDifficultyQueries()
|
||||
{
|
||||
const string query = "od>4 easy od<8";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.Greater(filterCriteria.OverallDifficulty.Min, 4.0);
|
||||
Assert.Less(filterCriteria.OverallDifficulty.Min, 4.1);
|
||||
Assert.Greater(filterCriteria.OverallDifficulty.Max, 7.9);
|
||||
Assert.Less(filterCriteria.OverallDifficulty.Max, 8.0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyBPMQueries()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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 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;
|
||||
using osu.Game.Tests.Testing;
|
||||
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();
|
||||
|
||||
[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()
|
||||
{
|
||||
Texture textureOnLoad = null;
|
||||
|
||||
AddStep("setup provider", () =>
|
||||
{
|
||||
var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin);
|
||||
|
||||
rulesetSkinProvider.Add(requester = new SkinRequester());
|
||||
|
||||
requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("test-image");
|
||||
|
||||
Child = rulesetSkinProvider;
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
public event Action OnLoadAsync;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
|
||||
OnLoadAsync?.Invoke();
|
||||
}
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS = default, WrapMode wrapModeT = default) => skin.GetTexture(componentName);
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
OnlineScoreID = 12345,
|
||||
};
|
||||
|
||||
var imported = await loadScoreIntoOsu(osu, toImport);
|
||||
var imported = await LoadScoreIntoOsu(osu, toImport);
|
||||
|
||||
Assert.AreEqual(toImport.Rank, imported.Rank);
|
||||
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
|
||||
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
|
||||
};
|
||||
|
||||
var imported = await loadScoreIntoOsu(osu, toImport);
|
||||
var imported = await LoadScoreIntoOsu(osu, toImport);
|
||||
|
||||
Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock));
|
||||
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
|
||||
@@ -105,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
}
|
||||
};
|
||||
|
||||
var imported = await loadScoreIntoOsu(osu, toImport);
|
||||
var imported = await LoadScoreIntoOsu(osu, toImport);
|
||||
|
||||
Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]);
|
||||
Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]);
|
||||
@@ -136,7 +136,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
}
|
||||
};
|
||||
|
||||
var imported = await loadScoreIntoOsu(osu, toImport);
|
||||
var imported = await LoadScoreIntoOsu(osu, toImport);
|
||||
|
||||
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
|
||||
var scoreManager = osu.Dependencies.Get<ScoreManager>();
|
||||
@@ -144,7 +144,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID)));
|
||||
Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true));
|
||||
|
||||
var secondImport = await loadScoreIntoOsu(osu, imported);
|
||||
var secondImport = await LoadScoreIntoOsu(osu, imported);
|
||||
Assert.That(secondImport, Is.Null);
|
||||
}
|
||||
finally
|
||||
@@ -163,7 +163,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
await loadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
|
||||
await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
|
||||
|
||||
var scoreManager = osu.Dependencies.Get<ScoreManager>();
|
||||
|
||||
@@ -177,7 +177,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ScoreInfo> loadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
|
||||
public static async Task<ScoreInfo> LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
|
||||
{
|
||||
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
|
||||
|
||||
@@ -190,7 +190,7 @@ namespace osu.Game.Tests.Scores.IO
|
||||
return scoreManager.GetAllUsableScores().FirstOrDefault();
|
||||
}
|
||||
|
||||
private class TestArchiveReader : ArchiveReader
|
||||
internal class TestArchiveReader : ArchiveReader
|
||||
{
|
||||
public TestArchiveReader()
|
||||
: base("test_archive")
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Testing
|
||||
Dependencies.Get<TestRulesetConfigManager>() != null);
|
||||
}
|
||||
|
||||
private class TestRuleset : Ruleset
|
||||
public class TestRuleset : Ruleset
|
||||
{
|
||||
public override string Description => string.Empty;
|
||||
public override string ShortName => string.Empty;
|
||||
|
||||
@@ -55,7 +55,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestExitWithoutSave()
|
||||
{
|
||||
AddStep("exit without save", () => Editor.Exit());
|
||||
AddStep("exit without save", () =>
|
||||
{
|
||||
Editor.Exit();
|
||||
DialogOverlay.CurrentDialog.PerformOkAction();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
|
||||
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
|
||||
}
|
||||
|
||||
@@ -116,12 +116,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private class TestOsuRuleset : OsuRuleset
|
||||
{
|
||||
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(source);
|
||||
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(skin);
|
||||
|
||||
private class TestOsuLegacySkinTransformer : OsuLegacySkinTransformer
|
||||
{
|
||||
public TestOsuLegacySkinTransformer(ISkinSource source)
|
||||
: base(source)
|
||||
public TestOsuLegacySkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user