mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 02:09:52 +08:00
Compare commits
691 Commits
@@ -82,8 +82,18 @@ jobs:
|
||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
||||
|
||||
- name: Test
|
||||
run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
|
||||
shell: pwsh
|
||||
run: >
|
||||
dotnet test
|
||||
osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll
|
||||
osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll
|
||||
osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll
|
||||
osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll
|
||||
osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll
|
||||
osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll
|
||||
Templates/**/*.Tests/bin/Debug/**/*.Tests.dll
|
||||
--logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
|
||||
--
|
||||
NUnit.ConsoleOut=0
|
||||
|
||||
# Attempt to upload results even if test fails.
|
||||
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
|
||||
@@ -136,4 +146,4 @@ jobs:
|
||||
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c Debug osu.iOS
|
||||
run: dotnet build -c Debug osu.iOS.slnf
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.225.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.418.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new CatchRuleset();
|
||||
|
||||
protected const double TIME_SNAP = 100;
|
||||
|
||||
protected DrawableCatchHitObject LastObject;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -16,26 +17,30 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
int fruits = HitObjects.Count(s => s is Fruit);
|
||||
int juiceStreams = HitObjects.Count(s => s is JuiceStream);
|
||||
int bananaShowers = HitObjects.Count(s => s is BananaShower);
|
||||
int sum = Math.Max(1, fruits + juiceStreams);
|
||||
|
||||
return new[]
|
||||
{
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Fruit Count",
|
||||
Name = @"Fruits",
|
||||
Content = fruits.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
|
||||
BarDisplayLength = fruits / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Juice Stream Count",
|
||||
Name = @"Juice Streams",
|
||||
Content = juiceStreams.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
|
||||
BarDisplayLength = juiceStreams / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Banana Shower Count",
|
||||
Name = @"Banana Showers",
|
||||
Content = bananaShowers.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
|
||||
BarDisplayLength = Math.Min(bananaShowers / 10f, 1),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
|
||||
TickDistanceMultiplier = beatmap.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||
}.Yield();
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
base.UpdateHitObjectFromPath(hitObject);
|
||||
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLengthForPlacement)
|
||||
EditorBeatmap?.Remove(hitObject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
@@ -89,6 +90,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
KeyboardStep = 1,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// 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 System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
@@ -35,21 +36,21 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
|
||||
public BindableBool HardRockOffsets { get; } = new BindableBool();
|
||||
|
||||
public override string SettingDescription
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
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";
|
||||
if (!CircleSize.IsDefault)
|
||||
yield return ("Circle size", $"{CircleSize.Value:N1}");
|
||||
|
||||
return string.Join(", ", new[]
|
||||
{
|
||||
circleSize,
|
||||
base.SettingDescription,
|
||||
approachRate,
|
||||
spicyPatterns,
|
||||
}.Where(s => !string.IsNullOrEmpty(s)));
|
||||
foreach (var setting in base.SettingDescription)
|
||||
yield return setting;
|
||||
|
||||
if (!ApproachRate.IsDefault)
|
||||
yield return ("Approach rate", $"{ApproachRate.Value:N1}");
|
||||
|
||||
if (!HardRockOffsets.IsDefault)
|
||||
yield return ("Spicy patterns", "On");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public abstract partial class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
|
||||
private readonly Column column;
|
||||
|
||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManiaFilterCriteriaTest
|
||||
{
|
||||
[TestCase]
|
||||
public void TestKeysEqualSingleValue()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1");
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysEqualMultipleValues()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1,3,5,7");
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysNotEqualSingleValue()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysNotEqualMultipleValues()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1,3,5,7");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysGreaterOrEqualThan()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey7()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestFilterIntersection()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "7");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 7 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 9 }),
|
||||
new FilterCriteria()));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestInvalidFilters()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
|
||||
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
|
||||
{
|
||||
protected override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
|
||||
protected override string? ExportLocation => null;
|
||||
|
||||
private static readonly object[][] score_v2_test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
|
||||
// Note that mania hitwindows are heavily idiosyncratic,
|
||||
// and if you *think* a number here is wrong, probably double check.
|
||||
|
||||
// Known issues / complexities:
|
||||
// - There is a disparate set of hitwindow ranges for: score V1 non-converts, score V1 converts, and score V2 (regardless of convert)
|
||||
// - It is NEVER POSSIBLE to get a MEH result when late; exceeding the OK hit windows will result in a MISS.
|
||||
// Additionally, the OK hit window when late is EXCLUSIVE / OPEN rather than INCLUSIVE / CLOSED.
|
||||
// Relevant stable source: https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/HitObjectManagerMania.cs#L737-L751
|
||||
// - There is also a seemingly mania-specific issue wherein key inputs registered before time instant 0 get truncated to time 0,
|
||||
// which is why the beatmaps used below make sure not to cross that boundary (the note starts at t=300ms).
|
||||
// This is not an issue in osu! or taiko.
|
||||
// The source of this behaviour has not been investigated in detail.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// PERFECT hit window is [ -19ms, 19ms]
|
||||
// GREAT hit window is [ -49ms, 49ms]
|
||||
// GOOD hit window is [ -82ms, 82ms]
|
||||
// OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -18d, HitResult.Perfect },
|
||||
new object[] { 5f, -19d, HitResult.Perfect },
|
||||
new object[] { 5f, -20d, HitResult.Great },
|
||||
new object[] { 5f, -21d, HitResult.Great },
|
||||
new object[] { 5f, -48d, HitResult.Great },
|
||||
new object[] { 5f, -49d, HitResult.Great },
|
||||
new object[] { 5f, -50d, HitResult.Good },
|
||||
new object[] { 5f, -51d, HitResult.Good },
|
||||
new object[] { 5f, -81d, HitResult.Good },
|
||||
new object[] { 5f, -82d, HitResult.Good },
|
||||
new object[] { 5f, -83d, HitResult.Ok },
|
||||
new object[] { 5f, -84d, HitResult.Ok },
|
||||
new object[] { 5f, -111d, HitResult.Ok },
|
||||
new object[] { 5f, -112d, HitResult.Ok },
|
||||
new object[] { 5f, -113d, HitResult.Meh },
|
||||
new object[] { 5f, -114d, HitResult.Meh },
|
||||
new object[] { 5f, -135d, HitResult.Meh },
|
||||
new object[] { 5f, -136d, HitResult.Meh },
|
||||
new object[] { 5f, -137d, HitResult.Miss },
|
||||
new object[] { 5f, -138d, HitResult.Miss },
|
||||
new object[] { 5f, 111d, HitResult.Ok },
|
||||
new object[] { 5f, 112d, HitResult.Miss },
|
||||
new object[] { 5f, 113d, HitResult.Miss },
|
||||
new object[] { 5f, 114d, HitResult.Miss },
|
||||
new object[] { 5f, 135d, HitResult.Miss },
|
||||
new object[] { 5f, 136d, HitResult.Miss },
|
||||
new object[] { 5f, 137d, HitResult.Miss },
|
||||
new object[] { 5f, 138d, HitResult.Miss },
|
||||
|
||||
// OD = 9.3 test cases.
|
||||
// PERFECT hit window is [ -14ms, 14ms]
|
||||
// GREAT hit window is [ -36ms, 36ms]
|
||||
// GOOD hit window is [ -69ms, 69ms]
|
||||
// OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 9.3f, 13d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 14d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 15d, HitResult.Great },
|
||||
new object[] { 9.3f, 16d, HitResult.Great },
|
||||
new object[] { 9.3f, 35d, HitResult.Great },
|
||||
new object[] { 9.3f, 36d, HitResult.Great },
|
||||
new object[] { 9.3f, 37d, HitResult.Good },
|
||||
new object[] { 9.3f, 38d, HitResult.Good },
|
||||
new object[] { 9.3f, 68d, HitResult.Good },
|
||||
new object[] { 9.3f, 69d, HitResult.Good },
|
||||
new object[] { 9.3f, 70d, HitResult.Ok },
|
||||
new object[] { 9.3f, 71d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98d, HitResult.Ok },
|
||||
new object[] { 9.3f, 99d, HitResult.Miss },
|
||||
new object[] { 9.3f, 100d, HitResult.Miss },
|
||||
new object[] { 9.3f, 101d, HitResult.Miss },
|
||||
new object[] { 9.3f, 122d, HitResult.Miss },
|
||||
new object[] { 9.3f, 123d, HitResult.Miss },
|
||||
new object[] { 9.3f, 124d, HitResult.Miss },
|
||||
new object[] { 9.3f, 125d, HitResult.Miss },
|
||||
new object[] { 9.3f, -98d, HitResult.Ok },
|
||||
new object[] { 9.3f, -99d, HitResult.Ok },
|
||||
new object[] { 9.3f, -100d, HitResult.Meh },
|
||||
new object[] { 9.3f, -101d, HitResult.Meh },
|
||||
new object[] { 9.3f, -122d, HitResult.Meh },
|
||||
new object[] { 9.3f, -123d, HitResult.Meh },
|
||||
new object[] { 9.3f, -124d, HitResult.Miss },
|
||||
new object[] { 9.3f, -125d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] score_v1_non_convert_test_cases =
|
||||
{
|
||||
// OD = 5 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -49ms, 49ms]
|
||||
// GOOD hit window is [ -82ms, 82ms]
|
||||
// OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -15d, HitResult.Perfect },
|
||||
new object[] { 5f, -16d, HitResult.Perfect },
|
||||
new object[] { 5f, -17d, HitResult.Great },
|
||||
new object[] { 5f, -18d, HitResult.Great },
|
||||
new object[] { 5f, -48d, HitResult.Great },
|
||||
new object[] { 5f, -49d, HitResult.Great },
|
||||
new object[] { 5f, -50d, HitResult.Good },
|
||||
new object[] { 5f, -51d, HitResult.Good },
|
||||
new object[] { 5f, -81d, HitResult.Good },
|
||||
new object[] { 5f, -82d, HitResult.Good },
|
||||
new object[] { 5f, -83d, HitResult.Ok },
|
||||
new object[] { 5f, -84d, HitResult.Ok },
|
||||
new object[] { 5f, -111d, HitResult.Ok },
|
||||
new object[] { 5f, -112d, HitResult.Ok },
|
||||
new object[] { 5f, -113d, HitResult.Meh },
|
||||
new object[] { 5f, -114d, HitResult.Meh },
|
||||
new object[] { 5f, -135d, HitResult.Meh },
|
||||
new object[] { 5f, -136d, HitResult.Meh },
|
||||
new object[] { 5f, -137d, HitResult.Miss },
|
||||
new object[] { 5f, -138d, HitResult.Miss },
|
||||
new object[] { 5f, 111d, HitResult.Ok },
|
||||
new object[] { 5f, 112d, HitResult.Miss },
|
||||
new object[] { 5f, 113d, HitResult.Miss },
|
||||
new object[] { 5f, 114d, HitResult.Miss },
|
||||
new object[] { 5f, 135d, HitResult.Miss },
|
||||
new object[] { 5f, 136d, HitResult.Miss },
|
||||
new object[] { 5f, 137d, HitResult.Miss },
|
||||
new object[] { 5f, 138d, HitResult.Miss },
|
||||
|
||||
// OD = 9.3 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -36ms, 36ms]
|
||||
// GOOD hit window is [ -69ms, 69ms]
|
||||
// OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 9.3f, 15d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 16d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 17d, HitResult.Great },
|
||||
new object[] { 9.3f, 18d, HitResult.Great },
|
||||
new object[] { 9.3f, 35d, HitResult.Great },
|
||||
new object[] { 9.3f, 36d, HitResult.Great },
|
||||
new object[] { 9.3f, 37d, HitResult.Good },
|
||||
new object[] { 9.3f, 38d, HitResult.Good },
|
||||
new object[] { 9.3f, 68d, HitResult.Good },
|
||||
new object[] { 9.3f, 69d, HitResult.Good },
|
||||
new object[] { 9.3f, 70d, HitResult.Ok },
|
||||
new object[] { 9.3f, 71d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98d, HitResult.Ok },
|
||||
new object[] { 9.3f, 99d, HitResult.Miss },
|
||||
new object[] { 9.3f, 100d, HitResult.Miss },
|
||||
new object[] { 9.3f, 101d, HitResult.Miss },
|
||||
new object[] { 9.3f, 122d, HitResult.Miss },
|
||||
new object[] { 9.3f, 123d, HitResult.Miss },
|
||||
new object[] { 9.3f, 124d, HitResult.Miss },
|
||||
new object[] { 9.3f, 125d, HitResult.Miss },
|
||||
new object[] { 9.3f, -98d, HitResult.Ok },
|
||||
new object[] { 9.3f, -99d, HitResult.Ok },
|
||||
new object[] { 9.3f, -100d, HitResult.Meh },
|
||||
new object[] { 9.3f, -101d, HitResult.Meh },
|
||||
new object[] { 9.3f, -122d, HitResult.Meh },
|
||||
new object[] { 9.3f, -123d, HitResult.Meh },
|
||||
new object[] { 9.3f, -124d, HitResult.Miss },
|
||||
new object[] { 9.3f, -125d, HitResult.Miss },
|
||||
|
||||
// OD = 3.1 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -54ms, 54ms]
|
||||
// GOOD hit window is [ -87ms, 87ms]
|
||||
// OK hit window is [-117ms, 117ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-141ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 3.1f, 15d, HitResult.Perfect },
|
||||
new object[] { 3.1f, 16d, HitResult.Perfect },
|
||||
new object[] { 3.1f, 17d, HitResult.Great },
|
||||
new object[] { 3.1f, 18d, HitResult.Great },
|
||||
new object[] { 3.1f, 53d, HitResult.Great },
|
||||
new object[] { 3.1f, 54d, HitResult.Great },
|
||||
new object[] { 3.1f, 55d, HitResult.Good },
|
||||
new object[] { 3.1f, 56d, HitResult.Good },
|
||||
new object[] { 3.1f, 86d, HitResult.Good },
|
||||
new object[] { 3.1f, 87d, HitResult.Good },
|
||||
new object[] { 3.1f, 88d, HitResult.Ok },
|
||||
new object[] { 3.1f, 89d, HitResult.Ok },
|
||||
new object[] { 3.1f, 116d, HitResult.Ok },
|
||||
new object[] { 3.1f, 117d, HitResult.Miss },
|
||||
new object[] { 3.1f, 118d, HitResult.Miss },
|
||||
new object[] { 3.1f, 119d, HitResult.Miss },
|
||||
new object[] { 3.1f, 140d, HitResult.Miss },
|
||||
new object[] { 3.1f, 141d, HitResult.Miss },
|
||||
new object[] { 3.1f, 142d, HitResult.Miss },
|
||||
new object[] { 3.1f, 143d, HitResult.Miss },
|
||||
new object[] { 3.1f, -116d, HitResult.Ok },
|
||||
new object[] { 3.1f, -117d, HitResult.Ok },
|
||||
new object[] { 3.1f, -118d, HitResult.Meh },
|
||||
new object[] { 3.1f, -119d, HitResult.Meh },
|
||||
new object[] { 3.1f, -140d, HitResult.Meh },
|
||||
new object[] { 3.1f, -141d, HitResult.Meh },
|
||||
new object[] { 3.1f, -142d, HitResult.Miss },
|
||||
new object[] { 3.1f, -143d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] score_v1_convert_test_cases =
|
||||
{
|
||||
// OD = 5 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -34ms, 34ms]
|
||||
// GOOD hit window is [ -67ms, 67ms]
|
||||
// OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -15d, HitResult.Perfect },
|
||||
new object[] { 5f, -16d, HitResult.Perfect },
|
||||
new object[] { 5f, -17d, HitResult.Great },
|
||||
new object[] { 5f, -18d, HitResult.Great },
|
||||
new object[] { 5f, -33d, HitResult.Great },
|
||||
new object[] { 5f, -34d, HitResult.Great },
|
||||
new object[] { 5f, -35d, HitResult.Good },
|
||||
new object[] { 5f, -36d, HitResult.Good },
|
||||
new object[] { 5f, -66d, HitResult.Good },
|
||||
new object[] { 5f, -67d, HitResult.Good },
|
||||
new object[] { 5f, -68d, HitResult.Ok },
|
||||
new object[] { 5f, -69d, HitResult.Ok },
|
||||
new object[] { 5f, -96d, HitResult.Ok },
|
||||
new object[] { 5f, -97d, HitResult.Ok },
|
||||
new object[] { 5f, -98d, HitResult.Meh },
|
||||
new object[] { 5f, -99d, HitResult.Meh },
|
||||
new object[] { 5f, -120d, HitResult.Meh },
|
||||
new object[] { 5f, -121d, HitResult.Meh },
|
||||
new object[] { 5f, -122d, HitResult.Miss },
|
||||
new object[] { 5f, -123d, HitResult.Miss },
|
||||
new object[] { 5f, 96d, HitResult.Ok },
|
||||
new object[] { 5f, 97d, HitResult.Miss },
|
||||
new object[] { 5f, 98d, HitResult.Miss },
|
||||
new object[] { 5f, 99d, HitResult.Miss },
|
||||
new object[] { 5f, 120d, HitResult.Miss },
|
||||
new object[] { 5f, 121d, HitResult.Miss },
|
||||
new object[] { 5f, 122d, HitResult.Miss },
|
||||
new object[] { 5f, 123d, HitResult.Miss },
|
||||
|
||||
// OD = 3.1 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -47ms, 47ms]
|
||||
// GOOD hit window is [ -77ms, 77ms]
|
||||
// OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 3.1f, 15d, HitResult.Perfect },
|
||||
new object[] { 3.1f, 16d, HitResult.Perfect },
|
||||
new object[] { 3.1f, 17d, HitResult.Great },
|
||||
new object[] { 3.1f, 18d, HitResult.Great },
|
||||
new object[] { 3.1f, 46d, HitResult.Great },
|
||||
new object[] { 3.1f, 47d, HitResult.Great },
|
||||
new object[] { 3.1f, 48d, HitResult.Good },
|
||||
new object[] { 3.1f, 49d, HitResult.Good },
|
||||
new object[] { 3.1f, 76d, HitResult.Good },
|
||||
new object[] { 3.1f, 77d, HitResult.Good },
|
||||
new object[] { 3.1f, 78d, HitResult.Ok },
|
||||
new object[] { 3.1f, 79d, HitResult.Ok },
|
||||
new object[] { 3.1f, 96d, HitResult.Ok },
|
||||
new object[] { 3.1f, 97d, HitResult.Miss },
|
||||
new object[] { 3.1f, 98d, HitResult.Miss },
|
||||
new object[] { 3.1f, 99d, HitResult.Miss },
|
||||
new object[] { 3.1f, 120d, HitResult.Miss },
|
||||
new object[] { 3.1f, 121d, HitResult.Miss },
|
||||
new object[] { 3.1f, 122d, HitResult.Miss },
|
||||
new object[] { 3.1f, 123d, HitResult.Miss },
|
||||
new object[] { 3.1f, -96d, HitResult.Ok },
|
||||
new object[] { 3.1f, -97d, HitResult.Ok },
|
||||
new object[] { 3.1f, -98d, HitResult.Meh },
|
||||
new object[] { 3.1f, -99d, HitResult.Meh },
|
||||
new object[] { 3.1f, -120d, HitResult.Meh },
|
||||
new object[] { 3.1f, -121d, HitResult.Meh },
|
||||
new object[] { 3.1f, -122d, HitResult.Miss },
|
||||
new object[] { 3.1f, -123d, HitResult.Miss },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(score_v2_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV2(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double note_time = 300;
|
||||
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(1))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = note_time,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
OverallDifficulty = overallDifficulty,
|
||||
CircleSize = 1,
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo,
|
||||
},
|
||||
ControlPointInfo = cpi,
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new ModScoreV2()]
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV2 single note @ OD{overallDifficulty}", beatmap, $@"SV2 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(score_v1_non_convert_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double note_time = 300;
|
||||
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(1))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = note_time,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
OverallDifficulty = overallDifficulty,
|
||||
CircleSize = 1,
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo,
|
||||
},
|
||||
ControlPointInfo = cpi,
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV1 single note @ OD{overallDifficulty}", beatmap, $@"SV1 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(score_v1_convert_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double note_time = 300;
|
||||
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new FakeCircle
|
||||
{
|
||||
StartTime = note_time,
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
OverallDifficulty = overallDifficulty,
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new RulesetInfo { OnlineID = 0 }
|
||||
},
|
||||
ControlPointInfo = cpi,
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new ManiaModKey1()],
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
private class FakeCircle : HitObject, IHasPosition
|
||||
{
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Position.Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(Position.X, value);
|
||||
}
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,13 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
@@ -14,6 +17,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() => toggleTouchControls(false));
|
||||
|
||||
#region Without touch controls
|
||||
|
||||
[Test]
|
||||
public void TestTouchInput()
|
||||
{
|
||||
@@ -63,6 +71,79 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region With touch controls
|
||||
|
||||
[Test]
|
||||
public void TestTouchAreaNotInitiallyVisible()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPressReceptors()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int index = i;
|
||||
|
||||
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(index).Action.Value));
|
||||
|
||||
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestColumnsNotTouchableWithTouchControls()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
|
||||
AddStep("touch receptor 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(0).Action.Value));
|
||||
|
||||
AddStep("release receptor 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f))));
|
||||
|
||||
AddAssert("action not sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
|
||||
AddStep("release column 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f))));
|
||||
|
||||
AddAssert("action not sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void toggleTouchControls(bool enabled)
|
||||
{
|
||||
var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!;
|
||||
maniaConfig.SetValue(ManiaRulesetSetting.MobileLayout, enabled ? ManiaMobileLayout.LandscapeWithOverlay : ManiaMobileLayout.Portrait);
|
||||
}
|
||||
|
||||
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
|
||||
|
||||
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
|
||||
|
||||
private Column getColumn(int index) => this.ChildrenOfType<Column>().ElementAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
|
||||
public partial class TestSceneReplayStability : ReplayStabilityTestScene
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// PERFECT hit window is [ -19.4ms, 19.4ms]
|
||||
// GREAT hit window is [ -49.0ms, 49.0ms]
|
||||
// GOOD hit window is [ -82.0ms, 82.0ms]
|
||||
// OK hit window is [-112.0ms, 112.0ms]
|
||||
// MEH hit window is [-136.0ms, 136.0ms]
|
||||
// MISS hit window is [-173.0ms, 173.0ms]
|
||||
new object[] { 5f, -19d, HitResult.Perfect },
|
||||
new object[] { 5f, -19.2d, HitResult.Perfect },
|
||||
new object[] { 5f, -19.38d, HitResult.Perfect },
|
||||
// new object[] { 5f, -19.4d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
|
||||
new object[] { 5f, -19.44d, HitResult.Great },
|
||||
new object[] { 5f, -19.7d, HitResult.Great },
|
||||
new object[] { 5f, -20d, HitResult.Great },
|
||||
new object[] { 5f, -48d, HitResult.Great },
|
||||
new object[] { 5f, -48.4d, HitResult.Great },
|
||||
new object[] { 5f, -48.7d, HitResult.Great },
|
||||
new object[] { 5f, -49d, HitResult.Great },
|
||||
new object[] { 5f, -49.2d, HitResult.Good },
|
||||
new object[] { 5f, -49.7d, HitResult.Good },
|
||||
new object[] { 5f, -50d, HitResult.Good },
|
||||
new object[] { 5f, -81d, HitResult.Good },
|
||||
new object[] { 5f, -81.2d, HitResult.Good },
|
||||
new object[] { 5f, -81.7d, HitResult.Good },
|
||||
new object[] { 5f, -82d, HitResult.Good },
|
||||
new object[] { 5f, -82.2d, HitResult.Ok },
|
||||
new object[] { 5f, -82.7d, HitResult.Ok },
|
||||
new object[] { 5f, -83d, HitResult.Ok },
|
||||
new object[] { 5f, -111d, HitResult.Ok },
|
||||
new object[] { 5f, -111.2d, HitResult.Ok },
|
||||
new object[] { 5f, -111.7d, HitResult.Ok },
|
||||
new object[] { 5f, -112d, HitResult.Ok },
|
||||
new object[] { 5f, -112.2d, HitResult.Meh },
|
||||
new object[] { 5f, -112.7d, HitResult.Meh },
|
||||
new object[] { 5f, -113d, HitResult.Meh },
|
||||
new object[] { 5f, -135d, HitResult.Meh },
|
||||
new object[] { 5f, -135.2d, HitResult.Meh },
|
||||
new object[] { 5f, -135.8d, HitResult.Meh },
|
||||
new object[] { 5f, -136d, HitResult.Meh },
|
||||
new object[] { 5f, -136.2d, HitResult.Miss },
|
||||
new object[] { 5f, -136.7d, HitResult.Miss },
|
||||
new object[] { 5f, -137d, HitResult.Miss },
|
||||
|
||||
// OD = 9.3 test cases.
|
||||
// PERFECT hit window is [ -14.67ms, 14.67ms]
|
||||
// GREAT hit window is [ -36.10ms, 36.10ms]
|
||||
// GOOD hit window is [ -69.10ms, 69.10ms]
|
||||
// OK hit window is [ -99.10ms, 99.10ms]
|
||||
// MEH hit window is [-123.10ms, 123.10ms]
|
||||
// MISS hit window is [-160.10ms, 160.10ms]
|
||||
new object[] { 9.3f, 14d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 14.2d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 14.6d, HitResult.Perfect },
|
||||
// new object[] { 9.3f, 14.67d, HitResult.Perfect }, <- in theory this should work, in practice it does not (fails even before encode & rounding due to floating point precision issues)
|
||||
new object[] { 9.3f, 14.7d, HitResult.Great },
|
||||
new object[] { 9.3f, 15d, HitResult.Great },
|
||||
new object[] { 9.3f, 35d, HitResult.Great },
|
||||
new object[] { 9.3f, 35.3d, HitResult.Great },
|
||||
new object[] { 9.3f, 35.8d, HitResult.Great },
|
||||
new object[] { 9.3f, 36.05d, HitResult.Great },
|
||||
new object[] { 9.3f, 36.3d, HitResult.Good },
|
||||
new object[] { 9.3f, 36.7d, HitResult.Good },
|
||||
new object[] { 9.3f, 37d, HitResult.Good },
|
||||
new object[] { 9.3f, 68d, HitResult.Good },
|
||||
new object[] { 9.3f, 68.4d, HitResult.Good },
|
||||
new object[] { 9.3f, 68.9d, HitResult.Good },
|
||||
new object[] { 9.3f, 69.07d, HitResult.Good },
|
||||
new object[] { 9.3f, 69.25d, HitResult.Ok },
|
||||
new object[] { 9.3f, 69.85d, HitResult.Ok },
|
||||
new object[] { 9.3f, 70d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98.3d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98.6d, HitResult.Ok },
|
||||
new object[] { 9.3f, 99d, HitResult.Ok },
|
||||
new object[] { 9.3f, 99.3d, HitResult.Meh },
|
||||
new object[] { 9.3f, 99.7d, HitResult.Meh },
|
||||
new object[] { 9.3f, 100d, HitResult.Meh },
|
||||
new object[] { 9.3f, 122d, HitResult.Meh },
|
||||
new object[] { 9.3f, 122.34d, HitResult.Meh },
|
||||
new object[] { 9.3f, 122.57d, HitResult.Meh },
|
||||
new object[] { 9.3f, 123.04d, HitResult.Meh },
|
||||
new object[] { 9.3f, 123.45d, HitResult.Miss },
|
||||
new object[] { 9.3f, 123.95d, HitResult.Miss },
|
||||
new object[] { 9.3f, 124d, HitResult.Miss },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double note_time = 100;
|
||||
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(1))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = note_time,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
OverallDifficulty = overallDifficulty,
|
||||
CircleSize = 1,
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo,
|
||||
},
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
RunTest(beatmap, replay, [expectedResult]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,20 +36,23 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
int notes = HitObjects.Count(s => s is Note);
|
||||
int holdNotes = HitObjects.Count(s => s is HoldNote);
|
||||
int sum = Math.Max(1, notes + holdNotes);
|
||||
|
||||
return new[]
|
||||
{
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Note Count",
|
||||
Name = @"Notes",
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
|
||||
Content = notes.ToString(),
|
||||
BarDisplayLength = notes / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Hold Note Count",
|
||||
Name = @"Hold Notes",
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
|
||||
Content = holdNotes.ToString(),
|
||||
BarDisplayLength = holdNotes / (float)sum,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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.Configuration.Tracking;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation;
|
||||
@@ -24,17 +23,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1);
|
||||
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
|
||||
|
||||
#pragma warning disable CS0618
|
||||
// Although obsolete, this is still required to populate the bindable from the database in case migration is required.
|
||||
SetDefault<double?>(ManiaRulesetSetting.ScrollTime, null);
|
||||
|
||||
if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime)
|
||||
{
|
||||
SetValue(ManiaRulesetSetting.ScrollSpeed, Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
|
||||
SetValue<double?>(ManiaRulesetSetting.ScrollTime, null);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
|
||||
}
|
||||
|
||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||
@@ -51,10 +40,9 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
|
||||
public enum ManiaRulesetSetting
|
||||
{
|
||||
[Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30
|
||||
ScrollTime,
|
||||
ScrollSpeed,
|
||||
ScrollDirection,
|
||||
TimingBasedNoteColouring
|
||||
TimingBasedNoteColouring,
|
||||
MobileLayout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
@@ -103,6 +104,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
KeyboardStep = 1,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
@@ -17,20 +18,72 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class ManiaFilterCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
private FilterCriteria.OptionalRange<float> keys;
|
||||
private readonly HashSet<int> includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet();
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
|
||||
{
|
||||
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods));
|
||||
int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods);
|
||||
|
||||
return includedKeyCounts.Contains(keyCount);
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "key":
|
||||
case "keys":
|
||||
return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value);
|
||||
{
|
||||
var keyCounts = new HashSet<int>();
|
||||
|
||||
foreach (string strValue in strValues.Split(','))
|
||||
{
|
||||
if (!int.TryParse(strValue, out int keyCount))
|
||||
return false;
|
||||
|
||||
keyCounts.Add(keyCount);
|
||||
}
|
||||
|
||||
int? singleKeyCount = keyCounts.Count == 1 ? keyCounts.Single() : null;
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case Operator.Equal:
|
||||
includedKeyCounts.IntersectWith(keyCounts);
|
||||
return true;
|
||||
|
||||
case Operator.NotEqual:
|
||||
includedKeyCounts.ExceptWith(keyCounts);
|
||||
return true;
|
||||
|
||||
case Operator.Less:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k >= singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
case Operator.LessOrEqual:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k > singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
case Operator.Greater:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k <= singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
case Operator.GreaterOrEqual:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k < singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -38,7 +91,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public bool FilterMayChangeFromMods(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
if (keys.HasFilter)
|
||||
if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT)
|
||||
{
|
||||
// Interpreting as the Mod type is required for equality comparison.
|
||||
HashSet<Mod> oldSet = mods.OldValue.OfType<ManiaKeyMod>().AsEnumerable<Mod>().ToHashSet();
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public enum ManiaMobileLayout
|
||||
{
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.PortraitExpandedColumns))]
|
||||
Portrait,
|
||||
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeExpandedColumns))]
|
||||
Landscape,
|
||||
|
||||
[LocalisableDescription(typeof(RulesetSettingsStrings), nameof(RulesetSettingsStrings.LandscapeTouchOverlay))]
|
||||
LandscapeWithOverlay,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
@@ -44,8 +45,17 @@ namespace osu.Game.Rulesets.Mania
|
||||
Keywords = new[] { "color" },
|
||||
LabelText = RulesetSettingsStrings.TimingBasedColouring,
|
||||
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (RuntimeInfo.IsMobile)
|
||||
{
|
||||
Add(new SettingsEnumDropdown<ManiaMobileLayout>
|
||||
{
|
||||
LabelText = RulesetSettingsStrings.MobileLayout,
|
||||
Current = config.GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ManiaScrollSlider : RoundedSliderBar<double>
|
||||
|
||||
@@ -42,8 +42,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
var locations = column.OfType<Note>().Select(n => (startTime: n.StartTime, samples: n.Samples))
|
||||
.Concat(column.OfType<HoldNote>().SelectMany(h => new[]
|
||||
{
|
||||
(startTime: h.StartTime, samples: h.GetNodeSamples(0)),
|
||||
(startTime: h.EndTime, samples: h.GetNodeSamples(1))
|
||||
(startTime: h.StartTime, samples: h.GetNodeSamples(0))
|
||||
}))
|
||||
.OrderBy(h => h.startTime).ToList();
|
||||
|
||||
|
||||
@@ -53,7 +53,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
}
|
||||
}
|
||||
|
||||
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
}
|
||||
|
||||
protected override SpriteText CreateJudgementText() =>
|
||||
new OsuSpriteText
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
this.result = result;
|
||||
this.animation = animation;
|
||||
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@@ -53,10 +52,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
|
||||
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
|
||||
|
||||
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
|
||||
float finalPosition = scorePosition - absoluteHitPosition;
|
||||
float hitPositionFromTop = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
|
||||
|
||||
Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition;
|
||||
if (scorePosition > hitPositionFromTop / 2f)
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Y = direction.Value == ScrollingDirection.Up ? hitPositionFromTop - scorePosition : scorePosition - hitPositionFromTop;
|
||||
}
|
||||
else
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.BottomCentre : Anchor.TopCentre;
|
||||
Y = direction.Value == ScrollingDirection.Up ? -scorePosition : scorePosition;
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayAnimation()
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
@@ -57,6 +58,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(Color4.Black);
|
||||
|
||||
private IBindable<ManiaMobileLayout> mobilePlayStyle = null!;
|
||||
|
||||
public Column(int index, bool isSpecial)
|
||||
{
|
||||
Index = index;
|
||||
@@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
private void load(GameHost host, ManiaRulesetConfigManager? rulesetConfig)
|
||||
{
|
||||
SkinnableDrawable keyArea;
|
||||
|
||||
@@ -115,6 +118,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
|
||||
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
|
||||
RegisterPool<HoldNoteBody, DrawableHoldNoteBody>(10, 50);
|
||||
|
||||
if (rulesetConfig != null)
|
||||
mobilePlayStyle = rulesetConfig.GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout);
|
||||
}
|
||||
|
||||
private void onSourceChanged()
|
||||
@@ -193,6 +199,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
// if touch overlay is visible, disallow columns from handling touch directly.
|
||||
if (mobilePlayStyle.Value == ManiaMobileLayout.LandscapeWithOverlay)
|
||||
return false;
|
||||
|
||||
maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
|
||||
touchActivationCount++;
|
||||
return true;
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
@@ -34,6 +39,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
set => base.Masking = value;
|
||||
}
|
||||
|
||||
private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
public ColumnFlow(StageDefinition stageDefinition)
|
||||
{
|
||||
this.stageDefinition = stageDefinition;
|
||||
@@ -52,42 +59,32 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
columns.Add(new Container<TContent> { RelativeSizeAxes = Axes.Y });
|
||||
|
||||
AddLayout(layout);
|
||||
}
|
||||
|
||||
private ISkinSource currentSkin;
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
private readonly Bindable<ManiaMobileLayout> mobileLayout = new Bindable<ManiaMobileLayout>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
private void load(ManiaRulesetConfigManager? rulesetConfig)
|
||||
{
|
||||
currentSkin = skin;
|
||||
rulesetConfig?.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout);
|
||||
|
||||
skin.SourceChanged += onSkinChanged;
|
||||
onSkinChanged();
|
||||
mobileLayout.BindValueChanged(_ => invalidateLayout());
|
||||
skin.SourceChanged += invalidateLayout;
|
||||
}
|
||||
|
||||
private void onSkinChanged()
|
||||
protected override void Update()
|
||||
{
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
base.Update();
|
||||
|
||||
if (!layout.IsValid)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
float spacing = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = spacing };
|
||||
}
|
||||
|
||||
float? width = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
|
||||
?.Value;
|
||||
|
||||
bool isSpecialColumn = stageDefinition.IsSpecialColumn(i);
|
||||
|
||||
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
|
||||
width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
|
||||
|
||||
columns[i].Width = width.Value;
|
||||
updateColumnSize();
|
||||
layout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,12 +98,60 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
Content[column] = columns[column].Child = content;
|
||||
}
|
||||
|
||||
private void invalidateLayout() => layout.Invalidate();
|
||||
|
||||
private void updateColumnSize()
|
||||
{
|
||||
float mobileAdjust = 1f;
|
||||
|
||||
if (RuntimeInfo.IsMobile && mobileLayout.Value == ManiaMobileLayout.Landscape)
|
||||
{
|
||||
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
|
||||
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
|
||||
|
||||
// Will be null in tests.
|
||||
if (containingCell != null && containingCell.Value.X >= containingCell.Value.Y)
|
||||
{
|
||||
float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
|
||||
|
||||
// 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
|
||||
mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
|
||||
// 1.92 is a "reference" mobile screen aspect ratio for phones.
|
||||
// We should scale it back for cases like tablets which aren't so extreme.
|
||||
mobileAdjust *= aspectRatio / 1.92f;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
float spacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = spacing };
|
||||
}
|
||||
|
||||
float? width = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
|
||||
?.Value;
|
||||
|
||||
bool isSpecialColumn = stageDefinition.IsSpecialColumn(i);
|
||||
|
||||
// only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration)
|
||||
width ??= isSpecialColumn ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH;
|
||||
|
||||
columns[i].Width = width.Value * mobileAdjust;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (currentSkin != null)
|
||||
currentSkin.SourceChanged -= onSkinChanged;
|
||||
if (skin.IsNotNull())
|
||||
skin.SourceChanged -= invalidateLayout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
|
||||
@@ -3,30 +3,23 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public partial class DrawableManiaJudgement : DrawableJudgement
|
||||
{
|
||||
private IBindable<ScrollingDirection> direction;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
public DrawableManiaJudgement()
|
||||
{
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => onDirectionChanged(), true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged()
|
||||
{
|
||||
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
Origin = Anchor.Centre;
|
||||
// Extend the dimensions of this drawable to the entire parenting container.
|
||||
// This allows skin implementations (i.e. LegacyManiaJudgementPiece) to freely choose the anchor based on skin settings.
|
||||
Anchor = Anchor.TopLeft;
|
||||
Origin = Anchor.TopLeft;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Size = new Vector2(1f);
|
||||
}
|
||||
|
||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public IEnumerable<BarLine> BarLines;
|
||||
|
||||
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1;
|
||||
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1 && mobileLayout.Value == ManiaMobileLayout.Portrait;
|
||||
|
||||
protected override bool RelativeScaleBeatLengths => true;
|
||||
|
||||
@@ -58,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||
private readonly BindableDouble configScrollSpeed = new BindableDouble();
|
||||
private readonly Bindable<ManiaMobileLayout> mobileLayout = new Bindable<ManiaMobileLayout>();
|
||||
|
||||
public double TargetTimeRange { get; protected set; }
|
||||
|
||||
private double currentTimeRange;
|
||||
protected double TargetTimeRange;
|
||||
|
||||
// Stores the current speed adjustment active in gameplay.
|
||||
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
|
||||
@@ -108,9 +110,37 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
|
||||
|
||||
Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
|
||||
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
|
||||
configScrollSpeed.BindValueChanged(speed =>
|
||||
{
|
||||
if (!AllowScrollSpeedAdjustment)
|
||||
return;
|
||||
|
||||
TargetTimeRange = ComputeScrollTime(speed.NewValue);
|
||||
});
|
||||
|
||||
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
||||
|
||||
Config.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout);
|
||||
mobileLayout.BindValueChanged(_ => updateMobileLayout(), true);
|
||||
}
|
||||
|
||||
private ManiaTouchInputArea? touchInputArea;
|
||||
|
||||
private void updateMobileLayout()
|
||||
{
|
||||
switch (mobileLayout.Value)
|
||||
{
|
||||
case ManiaMobileLayout.LandscapeWithOverlay:
|
||||
KeyBindingInputManager.Add(touchInputArea = new ManiaTouchInputArea(this));
|
||||
break;
|
||||
|
||||
default:
|
||||
if (touchInputArea != null)
|
||||
KeyBindingInputManager.Remove(touchInputArea, true);
|
||||
|
||||
touchInputArea = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
// 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.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// An overlay that captures and displays osu!mania mouse and touch input.
|
||||
/// </summary>
|
||||
public partial class ManiaTouchInputArea : VisibilityContainer
|
||||
{
|
||||
private readonly DrawableManiaRuleset drawableRuleset;
|
||||
|
||||
// visibility state affects our child. we always want to handle input.
|
||||
public override bool PropagatePositionalInputSubTree => true;
|
||||
public override bool PropagateNonPositionalInputSubTree => true;
|
||||
|
||||
[SettingSource("Spacing", "The spacing between receptors.")]
|
||||
public BindableFloat Spacing { get; } = new BindableFloat(10)
|
||||
{
|
||||
Precision = 1,
|
||||
MinValue = 0,
|
||||
MaxValue = 100,
|
||||
};
|
||||
|
||||
[SettingSource("Opacity", "The receptor opacity.")]
|
||||
public BindableFloat Opacity { get; } = new BindableFloat(1)
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 1
|
||||
};
|
||||
|
||||
private GridContainer gridContainer = null!;
|
||||
|
||||
public ManiaTouchInputArea(DrawableManiaRuleset drawableRuleset)
|
||||
{
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Height = 0.5f;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
List<Drawable> receptorGridContent = new List<Drawable>();
|
||||
List<Dimension> receptorGridDimensions = new List<Dimension>();
|
||||
|
||||
bool first = true;
|
||||
|
||||
foreach (var stage in drawableRuleset.Playfield.Stages)
|
||||
{
|
||||
foreach (var column in stage.Columns)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
|
||||
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
|
||||
}
|
||||
|
||||
receptorGridContent.Add(new ColumnInputReceptor
|
||||
{
|
||||
Action = { BindTarget = column.Action },
|
||||
});
|
||||
receptorGridDimensions.Add(new Dimension());
|
||||
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
InternalChild = gridContainer = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Content = new[] { receptorGridContent.ToArray() },
|
||||
ColumnDimensions = receptorGridDimensions.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
// Hide whenever the keyboard is used.
|
||||
Hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
Show();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
gridContainer.FadeIn(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
gridContainer.FadeOut(300);
|
||||
}
|
||||
|
||||
public partial class ColumnInputReceptor : CompositeDrawable
|
||||
{
|
||||
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||
|
||||
private readonly Box highlightOverlay;
|
||||
|
||||
[Resolved]
|
||||
private ManiaInputManager? inputManager { get; set; }
|
||||
|
||||
private bool isPressed;
|
||||
|
||||
public ColumnInputReceptor()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.15f,
|
||||
},
|
||||
highlightOverlay = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Blending = BlendingParameters.Additive,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
updateButton(true);
|
||||
return false; // handled by parent container to show overlay.
|
||||
}
|
||||
|
||||
protected override void OnTouchUp(TouchUpEvent e)
|
||||
{
|
||||
updateButton(false);
|
||||
}
|
||||
|
||||
private void updateButton(bool press)
|
||||
{
|
||||
if (press == isPressed)
|
||||
return;
|
||||
|
||||
isPressed = press;
|
||||
|
||||
if (press)
|
||||
{
|
||||
inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
|
||||
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
|
||||
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class Gutter : Drawable
|
||||
{
|
||||
public readonly IBindable<float> Spacing = new Bindable<float>();
|
||||
|
||||
public Gutter()
|
||||
{
|
||||
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
|
||||
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
|
||||
|
||||
@@ -759,9 +759,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
|
||||
},
|
||||
ControlPointInfo = cpi
|
||||
ControlPointInfo = cpi,
|
||||
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
|
||||
});
|
||||
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
|
||||
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
|
||||
{
|
||||
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
protected override string? ExportLocation => null;
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
// Additionally, note that offsets provided in double will be rounded to the nearest integer.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// GREAT hit window is ( -50ms, 50ms)
|
||||
// OK hit window is (-100ms, 100ms)
|
||||
// MEH hit window is (-150ms, 150ms)
|
||||
new object[] { 5f, 48d, HitResult.Great },
|
||||
new object[] { 5f, 49d, HitResult.Great },
|
||||
new object[] { 5f, 50d, HitResult.Ok },
|
||||
new object[] { 5f, 51d, HitResult.Ok },
|
||||
new object[] { 5f, 98d, HitResult.Ok },
|
||||
new object[] { 5f, 99d, HitResult.Ok },
|
||||
new object[] { 5f, 100d, HitResult.Meh },
|
||||
new object[] { 5f, 101d, HitResult.Meh },
|
||||
new object[] { 5f, 148d, HitResult.Meh },
|
||||
new object[] { 5f, 149d, HitResult.Meh },
|
||||
new object[] { 5f, 150d, HitResult.Miss },
|
||||
new object[] { 5f, 151d, HitResult.Miss },
|
||||
|
||||
// OD = 5.7 test cases.
|
||||
// GREAT hit window is ( -45ms, 45ms)
|
||||
// OK hit window is ( -94ms, 94ms)
|
||||
// MEH hit window is (-143ms, 143ms)
|
||||
new object[] { 5.7f, 43d, HitResult.Great },
|
||||
new object[] { 5.7f, 44d, HitResult.Great },
|
||||
new object[] { 5.7f, 45d, HitResult.Ok },
|
||||
new object[] { 5.7f, 46d, HitResult.Ok },
|
||||
new object[] { 5.7f, 92d, HitResult.Ok },
|
||||
new object[] { 5.7f, 93d, HitResult.Ok },
|
||||
new object[] { 5.7f, 94d, HitResult.Meh },
|
||||
new object[] { 5.7f, 95d, HitResult.Meh },
|
||||
new object[] { 5.7f, 141d, HitResult.Meh },
|
||||
new object[] { 5.7f, 142d, HitResult.Meh },
|
||||
new object[] { 5.7f, 143d, HitResult.Miss },
|
||||
new object[] { 5.7f, 144d, HitResult.Miss },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double hit_circle_time = 100;
|
||||
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||
var beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = hit_circle_time,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
},
|
||||
ControlPointInfo = cpi,
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
// required for correct playback in stable
|
||||
new OsuReplayFrame(0, new Vector2(256, -500)),
|
||||
new OsuReplayFrame(0, new Vector2(256, -500)),
|
||||
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"single circle @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
|
||||
public partial class TestSceneReplayStability : ReplayStabilityTestScene
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// GREAT hit window is [ -50ms, 50ms]
|
||||
// OK hit window is [-100ms, 100ms]
|
||||
// MEH hit window is [-150ms, 150ms]
|
||||
// MISS hit window is [-400ms, 400ms]
|
||||
new object[] { 5f, 49d, HitResult.Great },
|
||||
new object[] { 5f, 49.2d, HitResult.Great },
|
||||
new object[] { 5f, 49.7d, HitResult.Great },
|
||||
new object[] { 5f, 50d, HitResult.Great },
|
||||
new object[] { 5f, 50.4d, HitResult.Ok },
|
||||
new object[] { 5f, 50.9d, HitResult.Ok },
|
||||
new object[] { 5f, 51d, HitResult.Ok },
|
||||
new object[] { 5f, 99d, HitResult.Ok },
|
||||
new object[] { 5f, 99.2d, HitResult.Ok },
|
||||
new object[] { 5f, 99.7d, HitResult.Ok },
|
||||
new object[] { 5f, 100d, HitResult.Ok },
|
||||
new object[] { 5f, 100.4d, HitResult.Meh },
|
||||
new object[] { 5f, 100.9d, HitResult.Meh },
|
||||
new object[] { 5f, 101d, HitResult.Meh },
|
||||
new object[] { 5f, 149d, HitResult.Meh },
|
||||
new object[] { 5f, 149.2d, HitResult.Meh },
|
||||
new object[] { 5f, 149.7d, HitResult.Meh },
|
||||
new object[] { 5f, 150d, HitResult.Meh },
|
||||
new object[] { 5f, 150.4d, HitResult.Miss },
|
||||
new object[] { 5f, 150.9d, HitResult.Miss },
|
||||
new object[] { 5f, 151d, HitResult.Miss },
|
||||
|
||||
// OD = 5.7 test cases.
|
||||
// GREAT hit window is [ -45.8ms, 45.8ms]
|
||||
// OK hit window is [ -94.4ms, 94.4ms]
|
||||
// MEH hit window is [-143.0ms, 143.0ms]
|
||||
// MISS hit window is [-400.0ms, 400.0ms]
|
||||
new object[] { 5.7f, 45d, HitResult.Great },
|
||||
new object[] { 5.7f, 45.2d, HitResult.Great },
|
||||
new object[] { 5.7f, 45.8d, HitResult.Great },
|
||||
new object[] { 5.7f, 45.9d, HitResult.Ok },
|
||||
new object[] { 5.7f, 46d, HitResult.Ok },
|
||||
new object[] { 5.7f, 46.4d, HitResult.Ok },
|
||||
new object[] { 5.7f, 94d, HitResult.Ok },
|
||||
new object[] { 5.7f, 94.2d, HitResult.Ok },
|
||||
new object[] { 5.7f, 94.4d, HitResult.Ok },
|
||||
new object[] { 5.7f, 94.48d, HitResult.Ok },
|
||||
new object[] { 5.7f, 94.9d, HitResult.Meh },
|
||||
new object[] { 5.7f, 95d, HitResult.Meh },
|
||||
new object[] { 5.7f, 95.4d, HitResult.Meh },
|
||||
new object[] { 5.7f, 142d, HitResult.Meh },
|
||||
new object[] { 5.7f, 142.7d, HitResult.Meh },
|
||||
new object[] { 5.7f, 143d, HitResult.Meh },
|
||||
new object[] { 5.7f, 143.4d, HitResult.Miss },
|
||||
new object[] { 5.7f, 143.9d, HitResult.Miss },
|
||||
new object[] { 5.7f, 144d, HitResult.Miss },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double hit_circle_time = 100;
|
||||
|
||||
var beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = hit_circle_time,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
},
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2),
|
||||
}
|
||||
};
|
||||
|
||||
RunTest(beatmap, replay, [expectedResult]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,9 +86,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestSpinningSamplePitchShift()
|
||||
{
|
||||
PausableSkinnableSound spinSample = null;
|
||||
|
||||
AddStep("Add spinner", () => SetContents(_ => testSingle(5, true, 4000)));
|
||||
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
|
||||
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
|
||||
AddUntilStep("wait for spin sample", () => (spinSample = getSpinningSample()) != null);
|
||||
AddUntilStep("Pitch starts low", () => spinSample.Frequency.Value < 0.8);
|
||||
AddUntilStep("Pitch increases", () => spinSample.Frequency.Value > 0.8);
|
||||
|
||||
PausableSkinnableSound getSpinningSample() =>
|
||||
drawableSpinner.ChildrenOfType<PausableSkinnableSound>().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
@@ -16,26 +16,30 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
int circles = HitObjects.Count(c => c is HitCircle);
|
||||
int sliders = HitObjects.Count(s => s is Slider);
|
||||
int spinners = HitObjects.Count(s => s is Spinner);
|
||||
int sum = Math.Max(1, circles + sliders);
|
||||
|
||||
return new[]
|
||||
{
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = BeatmapsetsStrings.ShowStatsCountCircles,
|
||||
Name = "Circles",
|
||||
Content = circles.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
|
||||
BarDisplayLength = circles / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = BeatmapsetsStrings.ShowStatsCountSliders,
|
||||
Name = "Sliders",
|
||||
Content = sliders.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
|
||||
BarDisplayLength = sliders / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Spinner Count",
|
||||
Name = @"Spinners",
|
||||
Content = spinners.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
|
||||
BarDisplayLength = Math.Min(spinners / 10f, 1),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||
TickDistanceMultiplier = beatmap.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
|
||||
}.Yield();
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
foreach (var h in hitObjects)
|
||||
h.StackHeight = 0;
|
||||
|
||||
if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
|
||||
if (beatmap.BeatmapVersion >= 6)
|
||||
applyStacking(beatmap, hitObjects, 0, hitObjects.Count - 1);
|
||||
else
|
||||
applyStackingOld(beatmap, hitObjects);
|
||||
|
||||
+1
-1
@@ -484,7 +484,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// Snap the path to the current beat divisor before checking length validity.
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
if (!hitObject.Path.HasValidLength)
|
||||
if (!hitObject.Path.HasValidLengthForPlacement)
|
||||
{
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Position = oldControlPoints[i];
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLengthForPlacement;
|
||||
|
||||
public SliderPlacementBlueprint()
|
||||
: base(new Slider())
|
||||
|
||||
@@ -476,7 +476,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
|
||||
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)
|
||||
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLengthForPlacement)
|
||||
{
|
||||
placementHandler?.Delete(HitObject);
|
||||
return;
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public BindableFloat Spacing { get; } = new BindableFloat(4f)
|
||||
{
|
||||
MinValue = 4f,
|
||||
MaxValue = 128f,
|
||||
MaxValue = 256f,
|
||||
Precision = 0.01f,
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
@@ -351,6 +352,35 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
// Why is this logic here and not in `OsuSelectionHandler`?
|
||||
// Because we only want to handle this toggle after all other right-click handling completes.
|
||||
//
|
||||
// Consider that input is handled from the most nested child first:
|
||||
//
|
||||
// ComposeScreen
|
||||
// |- OsuContextMenuContainer // right click for context
|
||||
// |- TimelineBlueprintContainer
|
||||
// |- TimelineSelectionHandler
|
||||
// |- (Osu)HitObjectComposer // right click for toggle new combo
|
||||
// |- (Osu)EditorBlueprintContainer // right click for select
|
||||
// |- (Osu)EditorSelectionHandler // right click for delete
|
||||
if (e.Button == MouseButton.Right)
|
||||
{
|
||||
var osuSelectionHandler = (OsuSelectionHandler)BlueprintContainer.SelectionHandler;
|
||||
|
||||
if (!osuSelectionHandler.SelectedItems.Any())
|
||||
{
|
||||
osuSelectionHandler.SelectionNewComboState.Value =
|
||||
osuSelectionHandler.SelectionNewComboState.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLengthForPlacement)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Current = new BindableNumber<int>(3)
|
||||
{
|
||||
MinValue = 3,
|
||||
MaxValue = 10,
|
||||
MaxValue = 32,
|
||||
Precision = 1,
|
||||
},
|
||||
Instantaneous = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@@ -127,8 +128,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
if (relativeCheckbox.Current.Value)
|
||||
{
|
||||
(xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X);
|
||||
(yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y);
|
||||
xBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.X, 0);
|
||||
xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - Math.Min(initialSurroundingQuad.BottomRight.X, OsuPlayfield.BASE_SIZE.X);
|
||||
|
||||
yBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.Y, 0);
|
||||
yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - Math.Min(initialSurroundingQuad.BottomRight.Y, OsuPlayfield.BASE_SIZE.Y);
|
||||
|
||||
xBindable.Default = yBindable.Default = 0;
|
||||
|
||||
@@ -146,8 +150,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size);
|
||||
|
||||
(xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X);
|
||||
(yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y);
|
||||
if (initialSurroundingQuad.Width < OsuPlayfield.BASE_SIZE.X)
|
||||
{
|
||||
xBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.X;
|
||||
xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X;
|
||||
}
|
||||
else
|
||||
xBindable.MinValue = xBindable.MaxValue = initialPosition.X;
|
||||
|
||||
if (initialSurroundingQuad.Height < OsuPlayfield.BASE_SIZE.Y)
|
||||
{
|
||||
yBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.Y;
|
||||
yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y;
|
||||
}
|
||||
else
|
||||
yBindable.MinValue = yBindable.MaxValue = initialPosition.Y;
|
||||
|
||||
xBindable.Default = initialPosition.X;
|
||||
yBindable.Default = initialPosition.Y;
|
||||
|
||||
@@ -91,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
@@ -105,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
KeyboardStep = 1,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
@@ -119,6 +121,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
Caption = "Stack Leniency",
|
||||
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableFloat(Beatmap.StackLeniency)
|
||||
{
|
||||
Default = 0.7f,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -36,19 +36,18 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
|
||||
public override string SettingDescription
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
|
||||
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
|
||||
if (!CircleSize.IsDefault)
|
||||
yield return ("Circle size", $"{CircleSize.Value:N1}");
|
||||
|
||||
return string.Join(", ", new[]
|
||||
{
|
||||
circleSize,
|
||||
base.SettingDescription,
|
||||
approachRate
|
||||
}.Where(s => !string.IsNullOrEmpty(s)));
|
||||
foreach (var setting in base.SettingDescription)
|
||||
yield return setting;
|
||||
|
||||
if (!ApproachRate.IsDefault)
|
||||
yield return ("Approach rate", $"{ApproachRate.Value:N1}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
var spinner = (DrawableSpinner)drawable;
|
||||
|
||||
spinner.RotationTracker.Tracking = true;
|
||||
spinner.RotationTracker.Tracking = spinner.RotationTracker.IsSpinnableTime;
|
||||
|
||||
// early-return if we were paused to avoid division-by-zero in the subsequent calculations.
|
||||
if (Precision.AlmostEquals(spinner.Clock.Rate, 0))
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -149,5 +150,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
protected float CalculateDrawableRelativePosition(Drawable drawable) => (drawable.ScreenSpaceDrawQuad.Centre.X - parentScreenSpaceRectangle.X) / parentScreenSpaceRectangle.Width;
|
||||
|
||||
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
|
||||
|
||||
protected void ApplyRepeatFadeIn(Drawable target, double fadeTime)
|
||||
{
|
||||
DrawableSlider slider = (DrawableSlider)ParentHitObject;
|
||||
int repeatIndex = ((SliderEndCircle)HitObject).RepeatIndex;
|
||||
|
||||
Debug.Assert(slider != null);
|
||||
|
||||
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
|
||||
bool delayFadeIn = slider.SliderBody?.SnakingIn.Value == true && repeatIndex == 0;
|
||||
|
||||
if (repeatIndex > 0)
|
||||
fadeTime = Math.Min(slider.HitObject.SpanDuration, fadeTime);
|
||||
|
||||
target
|
||||
.FadeOut()
|
||||
.Delay(delayFadeIn ? (slider.HitObject.TimePreempt) / 3 : 0)
|
||||
.FadeIn(fadeTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||
|
||||
private double animDuration;
|
||||
|
||||
public SkinnableDrawable CirclePiece { get; private set; }
|
||||
|
||||
public SkinnableDrawable Arrow { get; private set; }
|
||||
@@ -87,21 +85,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
|
||||
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
animDuration = Math.Min(300, HitObject.SpanDuration);
|
||||
|
||||
this
|
||||
.FadeOut()
|
||||
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
|
||||
.FadeIn(HitObject.RepeatIndex == 0 ? HitObject.TimeFadeIn : animDuration);
|
||||
ApplyRepeatFadeIn(CirclePiece, HitObject.TimeFadeIn);
|
||||
ApplyRepeatFadeIn(Arrow, 150);
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
{
|
||||
base.UpdateHitStateTransforms(state);
|
||||
|
||||
double animDuration = Math.Min(300, HitObject.SpanDuration);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
|
||||
@@ -86,13 +86,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
|
||||
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
|
||||
|
||||
CirclePiece
|
||||
.FadeOut()
|
||||
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
|
||||
.FadeIn(HitObject.TimeFadeIn);
|
||||
ApplyRepeatFadeIn(CirclePiece, HitObject.TimeFadeIn);
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
|
||||
@@ -277,13 +277,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.Update();
|
||||
|
||||
if (HandleUserInput)
|
||||
{
|
||||
bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime;
|
||||
|
||||
RotationTracker.Tracking = !Result.HasResult
|
||||
&& correctButtonPressed()
|
||||
&& isValidSpinningTime;
|
||||
}
|
||||
RotationTracker.Tracking = RotationTracker.IsSpinnableTime && !Result.HasResult && correctButtonPressed();
|
||||
|
||||
if (spinningSample != null && spinnerFrequencyModulate)
|
||||
spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
|
||||
|
||||
@@ -85,9 +85,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||
|
||||
// When hit, don't animate further. This avoids a scale being applied on a scale and looking very weird.
|
||||
return;
|
||||
}
|
||||
else
|
||||
Scale = Vector2.One;
|
||||
|
||||
Scale = Vector2.One;
|
||||
|
||||
const float move_distance = -12;
|
||||
const float scale_amount = 1.3f;
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
/// <summary>
|
||||
/// Whether currently in the correct time range to allow spinning.
|
||||
/// </summary>
|
||||
private bool isSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current;
|
||||
public bool IsSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current;
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
@@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
lastAngle = thisAngle;
|
||||
}
|
||||
|
||||
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
|
||||
IsSpinning.Value = IsSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
|
||||
Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed));
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
/// <param name="delta">The delta angle.</param>
|
||||
public void AddRotation(float delta)
|
||||
{
|
||||
if (!isSpinnableTime)
|
||||
if (!IsSpinnableTime)
|
||||
return;
|
||||
|
||||
if (!rotationTransferred)
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
@@ -31,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
|
||||
AddStep("hover over first hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(1)));
|
||||
AddStep("hover over second hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(0)));
|
||||
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||
AddUntilStep("context menu open", () => Editor.ChildrenOfType<OsuContextMenu>().Any(menu => menu.State == MenuState.Open));
|
||||
AddUntilStep("second hit deleted", () => Editor.ChildrenOfType<DrawableHit>().Count(), () => Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
|
||||
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
|
||||
{
|
||||
protected override string? ExportLocation => null;
|
||||
|
||||
protected override Ruleset CreateRuleset() => new TaikoRuleset();
|
||||
|
||||
private static readonly object[][] test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// GREAT hit window is (-35ms, 35ms)
|
||||
// OK hit window is (-80ms, 80ms)
|
||||
new object[] { 5f, -33d, HitResult.Great },
|
||||
new object[] { 5f, -34d, HitResult.Great },
|
||||
new object[] { 5f, -35d, HitResult.Ok },
|
||||
new object[] { 5f, -36d, HitResult.Ok },
|
||||
new object[] { 5f, -78d, HitResult.Ok },
|
||||
new object[] { 5f, -79d, HitResult.Ok },
|
||||
new object[] { 5f, -80d, HitResult.Miss },
|
||||
new object[] { 5f, -81d, HitResult.Miss },
|
||||
|
||||
// OD = 7.8 test cases.
|
||||
// GREAT hit window is (-26ms, 26ms)
|
||||
// OK hit window is (-63ms, 63ms)
|
||||
new object[] { 7.8f, -24d, HitResult.Great },
|
||||
new object[] { 7.8f, -25d, HitResult.Great },
|
||||
new object[] { 7.8f, -26d, HitResult.Ok },
|
||||
new object[] { 7.8f, -27d, HitResult.Ok },
|
||||
new object[] { 7.8f, -61d, HitResult.Ok },
|
||||
new object[] { 7.8f, -62d, HitResult.Ok },
|
||||
new object[] { 7.8f, -63d, HitResult.Miss },
|
||||
new object[] { 7.8f, -64d, HitResult.Miss },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double hit_time = 100;
|
||||
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = hit_time,
|
||||
Type = HitType.Centre,
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new TaikoRuleset().RulesetInfo,
|
||||
},
|
||||
ControlPointInfo = cpi,
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(hit_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"single hit @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[Ignore("These tests are expected to fail until an acceptable solution for various replay playback issues concerning rounding of replay frame times & hit windows is found.")]
|
||||
public partial class TestSceneReplayStability : ReplayStabilityTestScene
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// GREAT hit window is [-35ms, 35ms]
|
||||
// OK hit window is [-80ms, 80ms]
|
||||
// MISS hit window is [-95ms, 95ms]
|
||||
new object[] { 5f, -34d, HitResult.Great },
|
||||
new object[] { 5f, -34.2d, HitResult.Great },
|
||||
new object[] { 5f, -34.7d, HitResult.Great },
|
||||
new object[] { 5f, -35d, HitResult.Great },
|
||||
new object[] { 5f, -35.2d, HitResult.Ok },
|
||||
new object[] { 5f, -35.8d, HitResult.Ok },
|
||||
new object[] { 5f, -36d, HitResult.Ok },
|
||||
new object[] { 5f, -79d, HitResult.Ok },
|
||||
new object[] { 5f, -79.3d, HitResult.Ok },
|
||||
new object[] { 5f, -79.7d, HitResult.Ok },
|
||||
new object[] { 5f, -80d, HitResult.Ok },
|
||||
new object[] { 5f, -80.2d, HitResult.Miss },
|
||||
new object[] { 5f, -80.8d, HitResult.Miss },
|
||||
new object[] { 5f, -81d, HitResult.Miss },
|
||||
|
||||
// OD = 7.8 test cases.
|
||||
// GREAT hit window is [-26.6ms, 26.6ms]
|
||||
// OK hit window is [-63.2ms, 63.2ms]
|
||||
// MISS hit window is [-81.0ms, 81.0ms]
|
||||
new object[] { 7.8f, -26d, HitResult.Great },
|
||||
new object[] { 7.8f, -26.4d, HitResult.Great },
|
||||
new object[] { 7.8f, -26.59d, HitResult.Great },
|
||||
new object[] { 7.8f, -26.8d, HitResult.Ok },
|
||||
new object[] { 7.8f, -27d, HitResult.Ok },
|
||||
new object[] { 7.8f, -27.1d, HitResult.Ok },
|
||||
new object[] { 7.8f, -63d, HitResult.Ok },
|
||||
new object[] { 7.8f, -63.18d, HitResult.Ok },
|
||||
new object[] { 7.8f, -63.4d, HitResult.Ok },
|
||||
new object[] { 7.8f, -63.7d, HitResult.Miss },
|
||||
new object[] { 7.8f, -64d, HitResult.Miss },
|
||||
new object[] { 7.8f, -64.2d, HitResult.Miss },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double hit_time = 100;
|
||||
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = hit_time,
|
||||
Type = HitType.Centre,
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new TaikoRuleset().RulesetInfo,
|
||||
},
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(hit_time + hitOffset, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(hit_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
RunTest(beatmap, replay, [expectedResult]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -15,26 +16,30 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
int hits = HitObjects.Count(s => s is Hit);
|
||||
int drumRolls = HitObjects.Count(s => s is DrumRoll);
|
||||
int swells = HitObjects.Count(s => s is Swell);
|
||||
int sum = Math.Max(1, hits + drumRolls);
|
||||
|
||||
return new[]
|
||||
{
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Hit Count",
|
||||
Name = @"Hits",
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
|
||||
Content = hits.ToString(),
|
||||
BarDisplayLength = hits / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Drumroll Count",
|
||||
Name = @"Drumrolls",
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
|
||||
Content = drumRolls.ToString(),
|
||||
BarDisplayLength = drumRolls / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Swell Count",
|
||||
Name = @"Swells",
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
|
||||
Content = swells.ToString(),
|
||||
BarDisplayLength = Math.Min(swells / 10f, 1),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
double osuVelocity = taikoVelocity * (1000f / beatLength);
|
||||
|
||||
// osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8
|
||||
if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
|
||||
if (beatmap.BeatmapVersion >= 8)
|
||||
beatLength = timingPoint.BeatLength;
|
||||
|
||||
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
|
||||
|
||||
@@ -13,25 +13,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the rhythm skill.
|
||||
/// </summary>
|
||||
[JsonProperty("rhythm_difficulty")]
|
||||
public double RhythmDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the reading skill.
|
||||
/// </summary>
|
||||
[JsonProperty("reading_difficulty")]
|
||||
public double ReadingDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the colour skill.
|
||||
/// </summary>
|
||||
[JsonProperty("colour_difficulty")]
|
||||
public double ColourDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the stamina skill.
|
||||
/// </summary>
|
||||
[JsonProperty("stamina_difficulty")]
|
||||
public double StaminaDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -40,13 +36,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
[JsonProperty("mono_stamina_factor")]
|
||||
public double MonoStaminaFactor { get; set; }
|
||||
|
||||
[JsonProperty("rhythm_difficult_strains")]
|
||||
public double RhythmTopStrains { get; set; }
|
||||
|
||||
[JsonProperty("colour_difficult_strains")]
|
||||
public double ColourTopStrains { get; set; }
|
||||
|
||||
[JsonProperty("stamina_difficult_strains")]
|
||||
public double StaminaTopStrains { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
@@ -74,6 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
KeyboardStep = 1,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
|
||||
@@ -1,7 +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.Linq;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -19,17 +20,15 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
ReadCurrentFromDifficulty = _ => 1,
|
||||
};
|
||||
|
||||
public override string SettingDescription
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N2}";
|
||||
foreach (var setting in base.SettingDescription)
|
||||
yield return setting;
|
||||
|
||||
return string.Join(", ", new[]
|
||||
{
|
||||
base.SettingDescription,
|
||||
scrollSpeed
|
||||
}.Where(s => !string.IsNullOrEmpty(s)));
|
||||
if (!ScrollSpeed.IsDefault)
|
||||
yield return ("Scroll speed", $"x{ScrollSpeed.Value:N2}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@@ -112,5 +113,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableHitObject.IsNotNull())
|
||||
drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
@@ -202,5 +203,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
||||
.Then()
|
||||
.FadeEdgeEffectTo(edge_alpha_kiai, duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableHitObject.IsNotNull())
|
||||
drawableHitObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
const double animation_time = 120;
|
||||
|
||||
(sprite as IFramedAnimation)?.GotoFrame(0);
|
||||
var animation = sprite as IFramedAnimation;
|
||||
|
||||
animation?.GotoFrame(0);
|
||||
(strongSprite as IFramedAnimation)?.GotoFrame(0);
|
||||
|
||||
this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5);
|
||||
|
||||
// legacy judgements don't play any transforms if they are an animation.
|
||||
if (animation?.FrameCount > 1)
|
||||
return;
|
||||
|
||||
this.ScaleTo(0.6f)
|
||||
.Then().ScaleTo(1.1f, animation_time * 0.8)
|
||||
.Then().ScaleTo(0.9f, animation_time * 0.4)
|
||||
|
||||
@@ -17,12 +17,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
internal partial class LegacyKiaiGlow : BeatSyncedContainer
|
||||
{
|
||||
private bool isKiaiActive;
|
||||
[Resolved]
|
||||
private HealthProcessor? healthProcessor { get; set; }
|
||||
|
||||
private bool isKiaiActive;
|
||||
private Sprite sprite = null!;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(ISkinSource skin, HealthProcessor? healthProcessor)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
Child = sprite = new Sprite
|
||||
{
|
||||
@@ -33,6 +35,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
Scale = new Vector2(TaikoLegacyHitTarget.SCALE),
|
||||
Colour = new Colour4(255, 228, 0, 255),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (healthProcessor != null)
|
||||
healthProcessor.NewJudgement += onNewJudgement;
|
||||
@@ -61,5 +68,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then()
|
||||
.ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (healthProcessor != null)
|
||||
healthProcessor.NewJudgement -= onNewJudgement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
@@ -21,9 +19,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? osuGame { get; set; }
|
||||
|
||||
public TaikoPlayfieldAdjustmentContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@@ -60,19 +55,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
|
||||
relativeHeight = Math.Min(relativeHeight, 1f / 3f);
|
||||
|
||||
Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
|
||||
|
||||
// on mobile platforms where the base aspect ratio is wider, the taiko playfield
|
||||
// needs to be scaled down to remain playable.
|
||||
if (RuntimeInfo.IsMobile && osuGame != null)
|
||||
{
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
// this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases.
|
||||
const float magic_scale = 1.25f;
|
||||
Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio);
|
||||
}
|
||||
|
||||
Scale = new Vector2(Parent!.ChildSize.Y / 768f * (relativeHeight / base_relative_height));
|
||||
Width = 1 / Scale.X;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Bogus" Version="35.6.2" />
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.iOS.props" />
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0-ios</TargetFramework>
|
||||
@@ -6,11 +7,19 @@
|
||||
<RootNamespace>osu.Game.Tests</RootNamespace>
|
||||
<AssemblyName>osu.Game.Tests.iOS</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<Import Project="..\osu.iOS.props" />
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);CA2007</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\osu.Game.Tests\**\*.cs" Exclude="**\obj\**">
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Compile>
|
||||
<!-- TargetPath is relative to RootNamespace,
|
||||
and DllResourceStore is relative to AssemblyName. -->
|
||||
<EmbeddedResource Include="..\osu.Game.Tests\**\Resources\**\*">
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<TargetPath>iOS\%(RecursiveDir)%(Filename)%(Extension)</TargetPath>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
@@ -20,6 +29,7 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Bogus" Version="35.6.2" />
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -42,9 +42,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
var decoder = Decoder.GetDecoder<Beatmap>(stream);
|
||||
var working = new TestWorkingBeatmap(decoder.Decode(stream));
|
||||
|
||||
Assert.AreEqual(6, working.BeatmapInfo.BeatmapVersion);
|
||||
Assert.AreEqual(6, working.Beatmap.BeatmapInfo.BeatmapVersion);
|
||||
Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapInfo.BeatmapVersion);
|
||||
Assert.AreEqual(6, working.Beatmap.BeatmapVersion);
|
||||
Assert.That(working.Beatmap.BeatmapInfo.Ruleset.Name, Is.Not.EqualTo("null placeholder ruleset"));
|
||||
Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
((LegacyBeatmapDecoder)decoder).ApplyOffsets = applyOffsets;
|
||||
var working = new TestWorkingBeatmap(decoder.Decode(stream));
|
||||
|
||||
Assert.AreEqual(4, working.BeatmapInfo.BeatmapVersion);
|
||||
Assert.AreEqual(4, working.Beatmap.BeatmapInfo.BeatmapVersion);
|
||||
Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapInfo.BeatmapVersion);
|
||||
Assert.AreEqual(4, working.Beatmap.BeatmapVersion);
|
||||
Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapVersion);
|
||||
|
||||
Assert.AreEqual(-1, working.BeatmapInfo.Metadata.PreviewTime);
|
||||
}
|
||||
|
||||
@@ -155,10 +155,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||
var beatmap = new TestBeatmap(ruleset)
|
||||
{
|
||||
BeatmapInfo =
|
||||
{
|
||||
BeatmapVersion = beatmapVersion
|
||||
}
|
||||
BeatmapVersion = beatmapVersion
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
@@ -633,14 +630,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
MD5Hash = md5Hash,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
Difficulty = new BeatmapDifficulty(),
|
||||
BeatmapVersion = beatmapVersion,
|
||||
},
|
||||
// needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die
|
||||
// needs to have at least one object so that `StandardisedScoreMigrationTools` doesn't die
|
||||
// when trying to recompute total score.
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle()
|
||||
}
|
||||
},
|
||||
BeatmapVersion = beatmapVersion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,24 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoopFadeTransformIsIgnoredForLifetime()
|
||||
{
|
||||
var decoder = new LegacyStoryboardDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("noop-fade-transform-is-ignored-for-lifetime.osb"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
|
||||
Assert.AreEqual(2, background.Elements.Count);
|
||||
|
||||
Assert.AreEqual(1500, background.Elements[0].StartTime);
|
||||
Assert.AreEqual(1500, background.Elements[1].StartTime);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOutOfOrderStartTimes()
|
||||
{
|
||||
@@ -288,6 +306,29 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVideoWithCustomFadeIn()
|
||||
{
|
||||
var decoder = new LegacyStoryboardDecoder();
|
||||
|
||||
using var resStream = TestResources.OpenResource("video-custom-alpha-transform.osb");
|
||||
using var stream = new LineBufferedReader(resStream);
|
||||
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1));
|
||||
Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf<StoryboardVideo>());
|
||||
Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678));
|
||||
Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().StartTime, Is.EqualTo(1500));
|
||||
Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().EndTime, Is.EqualTo(1600));
|
||||
|
||||
Assert.That(storyboard.EarliestEventTime, Is.Null);
|
||||
Assert.That(storyboard.LatestEventTime, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds()
|
||||
{
|
||||
|
||||
@@ -62,12 +62,11 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Run background processor", () =>
|
||||
{
|
||||
Add(new TestBackgroundDataStoreProcessor());
|
||||
});
|
||||
TestBackgroundDataStoreProcessor processor = null!;
|
||||
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
|
||||
AddUntilStep("Wait for completion", () => processor.Completed);
|
||||
|
||||
AddUntilStep("wait for difficulties repopulated", () =>
|
||||
AddAssert("Difficulties repopulated", () =>
|
||||
{
|
||||
return Realm.Run(r =>
|
||||
{
|
||||
@@ -101,13 +100,10 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Run background processor", () =>
|
||||
{
|
||||
Add(new TestBackgroundDataStoreProcessor());
|
||||
});
|
||||
TestBackgroundDataStoreProcessor processor = null!;
|
||||
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
|
||||
|
||||
AddWaitStep("wait some", 500);
|
||||
|
||||
AddAssert("Difficulty still not populated", () =>
|
||||
{
|
||||
return Realm.Run(r =>
|
||||
@@ -118,8 +114,9 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
|
||||
AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying);
|
||||
AddUntilStep("Wait for completion", () => processor.Completed);
|
||||
|
||||
AddUntilStep("wait for difficulties repopulated", () =>
|
||||
AddAssert("Difficulties repopulated", () =>
|
||||
{
|
||||
return Realm.Run(r =>
|
||||
{
|
||||
@@ -151,9 +148,11 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
|
||||
TestBackgroundDataStoreProcessor processor = null!;
|
||||
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
|
||||
AddUntilStep("Wait for completion", () => processor.Completed);
|
||||
|
||||
AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
|
||||
AddAssert("Score version upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
|
||||
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
|
||||
}
|
||||
|
||||
@@ -183,7 +182,7 @@ namespace osu.Game.Tests.Database
|
||||
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
|
||||
AddUntilStep("Wait for completion", () => processor.Completed);
|
||||
|
||||
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
|
||||
AddAssert("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
|
||||
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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.Extensions;
|
||||
|
||||
namespace osu.Game.Tests.Extensions
|
||||
{
|
||||
[TestFixture]
|
||||
public class NumberFormattingExtensionsTest
|
||||
{
|
||||
[TestCase(-1, false, 0, ExpectedResult = "-1")]
|
||||
[TestCase(0, false, 0, ExpectedResult = "0")]
|
||||
[TestCase(1, false, 0, ExpectedResult = "1")]
|
||||
[TestCase(500, false, 10, ExpectedResult = "500")]
|
||||
[TestCase(-1, true, 0, ExpectedResult = "-1%")]
|
||||
[TestCase(0, true, 0, ExpectedResult = "0%")]
|
||||
[TestCase(1, true, 0, ExpectedResult = "1%")]
|
||||
[TestCase(50, true, 0, ExpectedResult = "50%")]
|
||||
public string TestInteger(int input, bool percent, int decimalDigits)
|
||||
{
|
||||
return input.ToStandardFormattedString(decimalDigits, percent);
|
||||
}
|
||||
|
||||
[TestCase(-1, false, 0, ExpectedResult = "-1")]
|
||||
[TestCase(-1e-6, false, 0, ExpectedResult = "0")]
|
||||
[TestCase(-1e-6, false, 6, ExpectedResult = "-0.000001")]
|
||||
[TestCase(0, false, 10, ExpectedResult = "0")]
|
||||
[TestCase(0, false, 0, ExpectedResult = "0")]
|
||||
[TestCase(double.NegativeZero, false, 0, ExpectedResult = "0")]
|
||||
[TestCase(1e-6, false, 0, ExpectedResult = "0")]
|
||||
[TestCase(1e-6, false, 6, ExpectedResult = "0.000001")]
|
||||
[TestCase(1, false, 0, ExpectedResult = "1")]
|
||||
[TestCase(1.528, false, 2, ExpectedResult = "1.53")]
|
||||
[TestCase(500, false, 10, ExpectedResult = "500")]
|
||||
[TestCase(-0.1, true, 0, ExpectedResult = "-10%")]
|
||||
[TestCase(0, true, 0, ExpectedResult = "0%")]
|
||||
[TestCase(0.4, true, 0, ExpectedResult = "40%")]
|
||||
[TestCase(0.48333, true, 2, ExpectedResult = "48%")]
|
||||
[TestCase(0.48333, true, 4, ExpectedResult = "48.33%")]
|
||||
[TestCase(1, true, 0, ExpectedResult = "100%")]
|
||||
public string TestDouble(double input, bool percent, int decimalDigits)
|
||||
{
|
||||
return input.ToStandardFormattedString(decimalDigits, percent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SetCulture("fr-FR")]
|
||||
public void TestCultureInsensitivity()
|
||||
{
|
||||
Assert.That(0.4.ToStandardFormattedString(maxDecimalDigits: 2, asPercentage: true), Is.EqualTo("40%"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Tests.Online
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneMultiplayerBeatmapAvailabilityTracker : MultiplayerTestScene
|
||||
{
|
||||
private BeatmapManager beatmapManager = null!;
|
||||
private BeatmapInfo availableBeatmap = null!;
|
||||
private BeatmapInfo unavailableBeatmap = null!;
|
||||
|
||||
private MultiplayerBeatmapAvailabilityTracker tracker = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
{
|
||||
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
|
||||
var importedSet = beatmapManager.GetAllUsableBeatmapSets().First();
|
||||
availableBeatmap = importedSet.Beatmaps[0];
|
||||
unavailableBeatmap = importedSet.Beatmaps[1];
|
||||
|
||||
Realm.Write(r => r.Remove(r.Find<BeatmapInfo>(unavailableBeatmap.ID)!));
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("setup tracker", () =>
|
||||
{
|
||||
DummyAPIAccess api = (DummyAPIAccess)API;
|
||||
Func<APIRequest, bool>? defaultRequestHandler = api.HandleRequest;
|
||||
|
||||
api.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetBeatmapsRequest beatmapsReq:
|
||||
var availableApiBeatmap = CreateAPIBeatmap();
|
||||
availableApiBeatmap.OnlineID = availableBeatmap.OnlineID;
|
||||
availableApiBeatmap.OnlineBeatmapSetID = availableBeatmap.BeatmapSet!.OnlineID;
|
||||
availableApiBeatmap.Checksum = availableBeatmap.MD5Hash;
|
||||
availableApiBeatmap.BeatmapSet!.OnlineID = availableBeatmap.BeatmapSet!.OnlineID;
|
||||
|
||||
var unavailableApiBeatmap = CreateAPIBeatmap();
|
||||
unavailableApiBeatmap.OnlineID = unavailableBeatmap.OnlineID;
|
||||
unavailableApiBeatmap.OnlineBeatmapSetID = unavailableBeatmap.BeatmapSet!.OnlineID;
|
||||
unavailableApiBeatmap.Checksum = unavailableBeatmap.MD5Hash;
|
||||
unavailableApiBeatmap.BeatmapSet!.OnlineID = unavailableBeatmap.BeatmapSet!.OnlineID;
|
||||
|
||||
beatmapsReq.TriggerSuccess(new GetBeatmapsResponse
|
||||
{
|
||||
Beatmaps = new List<APIBeatmap>
|
||||
{
|
||||
availableApiBeatmap,
|
||||
unavailableApiBeatmap
|
||||
}
|
||||
});
|
||||
return true;
|
||||
|
||||
default:
|
||||
return defaultRequestHandler?.Invoke(req) ?? false;
|
||||
}
|
||||
};
|
||||
|
||||
Child = tracker = new MultiplayerBeatmapAvailabilityTracker();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEnterRoomWithNotDownloadedBeatmap()
|
||||
{
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom();
|
||||
room.Playlist = [new PlaylistItem(unavailableBeatmap)];
|
||||
JoinRoom(room);
|
||||
});
|
||||
|
||||
WaitForJoined();
|
||||
|
||||
AddUntilStep("beatmap is not available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.NotDownloaded));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEnterRoomWithLocallyAvailableBeatmap()
|
||||
{
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom();
|
||||
room.Playlist = [new PlaylistItem(availableBeatmap)];
|
||||
JoinRoom(room);
|
||||
});
|
||||
|
||||
WaitForJoined();
|
||||
|
||||
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAvailabilityUpdatesOnItemEdit()
|
||||
{
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom();
|
||||
room.Playlist = [new PlaylistItem(availableBeatmap)];
|
||||
JoinRoom(room);
|
||||
});
|
||||
|
||||
WaitForJoined();
|
||||
|
||||
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
|
||||
|
||||
AddStep("change item to not downloaded beatmap", () =>
|
||||
{
|
||||
PlaylistItem newItem = new PlaylistItem(MultiplayerClient.ClientRoom!.CurrentPlaylistItem).With(beatmap: new Optional<IBeatmapInfo>(unavailableBeatmap));
|
||||
MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(newItem)).WaitSafely();
|
||||
});
|
||||
|
||||
AddUntilStep("beatmap is not available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.NotDownloaded));
|
||||
|
||||
AddStep("change item to downloaded beatmap", () =>
|
||||
{
|
||||
PlaylistItem newItem = new PlaylistItem(MultiplayerClient.ClientRoom!.CurrentPlaylistItem).With(beatmap: new Optional<IBeatmapInfo>(availableBeatmap));
|
||||
MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(newItem)).WaitSafely();
|
||||
});
|
||||
|
||||
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAvailabilityUpdatesOnSettingsChange()
|
||||
{
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom();
|
||||
room.Playlist = [new PlaylistItem(availableBeatmap), new PlaylistItem(unavailableBeatmap)];
|
||||
JoinRoom(room);
|
||||
});
|
||||
|
||||
WaitForJoined();
|
||||
|
||||
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
|
||||
|
||||
AddStep("change settings to not downloaded beatmap", () => MultiplayerClient.ChangeServerRoomSettings(new MultiplayerRoomSettings(MultiplayerClient.ClientAPIRoom!)
|
||||
{
|
||||
PlaylistItemId = MultiplayerClient.ServerRoom!.Playlist[1].ID
|
||||
}).WaitSafely());
|
||||
|
||||
AddUntilStep("beatmap is not available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.NotDownloaded));
|
||||
|
||||
AddStep("change settings to downloaded beatmap", () => MultiplayerClient.ChangeServerRoomSettings(new MultiplayerRoomSettings(MultiplayerClient.ClientAPIRoom!)
|
||||
{
|
||||
PlaylistItemId = MultiplayerClient.ServerRoom!.Playlist[0].ID
|
||||
}).WaitSafely());
|
||||
|
||||
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-31
@@ -1,18 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.IO.Stores;
|
||||
@@ -27,31 +23,29 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Online
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneOnlinePlayBeatmapAvailabilityTracker : OsuTestScene
|
||||
public partial class TestScenePlaylistsBeatmapAvailabilityTracker : OsuTestScene
|
||||
{
|
||||
private RulesetStore rulesets;
|
||||
private TestBeatmapManager beatmaps;
|
||||
private TestBeatmapModelDownloader beatmapDownloader;
|
||||
private TestBeatmapManager beatmaps = null!;
|
||||
private TestBeatmapModelDownloader beatmapDownloader = null!;
|
||||
|
||||
private string testBeatmapFile;
|
||||
private BeatmapInfo testBeatmapInfo;
|
||||
private BeatmapSetInfo testBeatmapSet;
|
||||
private string testBeatmapFile = null!;
|
||||
private BeatmapInfo testBeatmapInfo = null!;
|
||||
private BeatmapSetInfo testBeatmapSet = null!;
|
||||
|
||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||
private OnlinePlayBeatmapAvailabilityTracker availabilityTracker;
|
||||
private OnlinePlayBeatmapAvailabilityTracker availabilityTracker = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio, GameHost host)
|
||||
{
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.CacheAs<BeatmapManager>(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.CacheAs<BeatmapManager>(beatmaps = new TestBeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API));
|
||||
}
|
||||
|
||||
@@ -82,16 +76,11 @@ namespace osu.Game.Tests.Online
|
||||
testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
|
||||
|
||||
testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile);
|
||||
testBeatmapSet = testBeatmapInfo.BeatmapSet;
|
||||
testBeatmapSet = testBeatmapInfo.BeatmapSet!;
|
||||
|
||||
Realm.Write(r => r.RemoveAll<BeatmapSetInfo>());
|
||||
Realm.Write(r => r.RemoveAll<BeatmapInfo>());
|
||||
|
||||
selectedItem.Value = new PlaylistItem(testBeatmapInfo)
|
||||
{
|
||||
RulesetID = testBeatmapInfo.Ruleset.OnlineID,
|
||||
};
|
||||
|
||||
recreateChildren();
|
||||
});
|
||||
|
||||
@@ -108,9 +97,15 @@ namespace osu.Game.Tests.Online
|
||||
Children = new Drawable[]
|
||||
{
|
||||
beatmapLookupCache,
|
||||
availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
|
||||
availabilityTracker = new PlaylistsBeatmapAvailabilityTracker
|
||||
{
|
||||
SelectedItem = { BindTarget = selectedItem, }
|
||||
PlaylistItem =
|
||||
{
|
||||
Value = new PlaylistItem(testBeatmapInfo)
|
||||
{
|
||||
RulesetID = testBeatmapInfo.Ruleset.OnlineID,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -125,10 +120,10 @@ namespace osu.Game.Tests.Online
|
||||
AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet));
|
||||
addAvailabilityCheckStep("state downloading 0%", () => BeatmapAvailability.Downloading(0.0f));
|
||||
|
||||
AddStep("set progress 40%", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.SetProgress(0.4f));
|
||||
AddStep("set progress 40%", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)!).SetProgress(0.4f));
|
||||
addAvailabilityCheckStep("state downloading 40%", () => BeatmapAvailability.Downloading(0.4f));
|
||||
|
||||
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
|
||||
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)!).TriggerSuccess(testBeatmapFile));
|
||||
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
|
||||
|
||||
AddStep("allow importing", () => beatmaps.AllowImport.Set());
|
||||
@@ -203,10 +198,10 @@ namespace osu.Game.Tests.Online
|
||||
{
|
||||
public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim();
|
||||
|
||||
public Live<BeatmapSetInfo> CurrentImport { get; private set; }
|
||||
public Live<BeatmapSetInfo>? CurrentImport { get; private set; }
|
||||
|
||||
public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources,
|
||||
GameHost host = null, WorkingBeatmap defaultBeatmap = null)
|
||||
public TestBeatmapManager(Storage storage, RealmAccess realm, IAPIProvider api, AudioManager audioManager, IResourceStore<byte[]> resources,
|
||||
GameHost? host = null, WorkingBeatmap? defaultBeatmap = null)
|
||||
: base(storage, realm, api, audioManager, resources, host, defaultBeatmap)
|
||||
{
|
||||
}
|
||||
@@ -226,12 +221,13 @@ namespace osu.Game.Tests.Online
|
||||
this.testBeatmapManager = testBeatmapManager;
|
||||
}
|
||||
|
||||
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default)
|
||||
public override Live<BeatmapSetInfo>? ImportModel(BeatmapSetInfo item, ArchiveReader? archive = null, ImportParameters parameters = default,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
|
||||
throw new TimeoutException("Timeout waiting for import to be allowed.");
|
||||
|
||||
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, parameters, cancellationToken));
|
||||
return testBeatmapManager.CurrentImport = base.ImportModel(item, archive, parameters, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 System;
|
||||
using Bogus;
|
||||
using MessagePack;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Tests.OnlinePlay
|
||||
{
|
||||
[TestFixture]
|
||||
public class MultiplayerPlaylistItemTest
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Randomizer.Seed = new Random(1337);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCloneMultiplayerPlaylistItem()
|
||||
{
|
||||
var faker = new Faker<MultiplayerPlaylistItem>()
|
||||
.StrictMode(true)
|
||||
.RuleFor(o => o.ID, f => f.Random.Long())
|
||||
.RuleFor(o => o.OwnerID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash())
|
||||
.RuleFor(o => o.RulesetID, f => f.Random.Int())
|
||||
.RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.Expired, f => f.Random.Bool())
|
||||
.RuleFor(o => o.PlaylistOrder, f => f.Random.UShort())
|
||||
.RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset())
|
||||
.RuleFor(o => o.StarRating, f => f.Random.Double())
|
||||
.RuleFor(o => o.Freestyle, f => f.Random.Bool());
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
MultiplayerPlaylistItem item = faker.Generate();
|
||||
Assert.That(MessagePackSerializer.SerializeToJson(item.Clone()), Is.EqualTo(MessagePackSerializer.SerializeToJson(item)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConstructFromAPIModel()
|
||||
{
|
||||
var faker = new Faker<MultiplayerPlaylistItem>()
|
||||
.StrictMode(true)
|
||||
.RuleFor(o => o.ID, f => f.Random.Long())
|
||||
.RuleFor(o => o.OwnerID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash())
|
||||
.RuleFor(o => o.RulesetID, f => f.Random.Int())
|
||||
.RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.Expired, f => f.Random.Bool())
|
||||
.RuleFor(o => o.PlaylistOrder, f => f.Random.UShort())
|
||||
.RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset())
|
||||
.RuleFor(o => o.StarRating, f => f.Random.Double())
|
||||
.RuleFor(o => o.Freestyle, f => f.Random.Bool());
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
MultiplayerPlaylistItem initialItem = faker.Generate();
|
||||
MultiplayerPlaylistItem copiedItem = new MultiplayerPlaylistItem(new PlaylistItem(initialItem));
|
||||
Assert.That(MessagePackSerializer.SerializeToJson(copiedItem), Is.EqualTo(MessagePackSerializer.SerializeToJson(initialItem)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
|
||||
namespace osu.Game.Tests.OnlinePlay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class TestSceneOnlinePlaySubScreenStack : OnlinePlayTestScene
|
||||
{
|
||||
private ScreenStack stack = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Child = stack = new OnlinePlaySubScreenStack
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBindablesDisabledWhenRequested()
|
||||
{
|
||||
AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False);
|
||||
|
||||
AddStep("push screen that disables bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(true)));
|
||||
AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True);
|
||||
|
||||
AddStep("push screen that does not disable bindables", () => stack.Push(new ScreenWithExternalBindableDisablement(false)));
|
||||
AddAssert("bindables not disabled", () => Beatmap.Disabled || Ruleset.Disabled || SelectedMods.Disabled, () => Is.False);
|
||||
|
||||
AddStep("exit one screen", () => stack.Exit());
|
||||
AddAssert("bindables disabled", () => Beatmap.Disabled && Ruleset.Disabled && SelectedMods.Disabled, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModsResetWhenExitToLounge()
|
||||
{
|
||||
AddStep("push lounge", () => stack.Push(new PlaylistsLoungeSubScreen()));
|
||||
|
||||
AddStep("push screen with mod", () => stack.Push(new ScreenWithMod(new OsuModDoubleTime())));
|
||||
AddUntilStep("wait for screen to load", () => ((OsuScreen)stack.CurrentScreen).IsLoaded);
|
||||
AddAssert("mod set", () => SelectedMods.Value.Count, () => Is.GreaterThan(0));
|
||||
|
||||
AddStep("exit to lounge", () => stack.Exit());
|
||||
AddAssert("mods reset", () => SelectedMods.Value.Count, () => Is.Zero);
|
||||
}
|
||||
|
||||
private partial class ScreenWithExternalBindableDisablement : OsuScreen
|
||||
{
|
||||
public override bool DisallowExternalBeatmapRulesetChanges { get; }
|
||||
|
||||
public ScreenWithExternalBindableDisablement(bool disableBindables)
|
||||
{
|
||||
DisallowExternalBeatmapRulesetChanges = disableBindables;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ScreenWithMod : OsuScreen
|
||||
{
|
||||
private readonly Mod mod;
|
||||
|
||||
public ScreenWithMod(Mod mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Mods.Value = [mod];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[Events]
|
||||
//Storyboard Layer 0 (Background)
|
||||
Sprite,Background,TopCentre,"img.jpg",320,240
|
||||
F,0,1000,1000,0,0 // should be ignored
|
||||
F,0,1500,1600,0,1
|
||||
Sprite,Background,TopCentre,"img.jpg",320,240
|
||||
F,0,1000,1000,0,0 // should be ignored
|
||||
F,0,1500,1600,1,1
|
||||
@@ -0,0 +1,5 @@
|
||||
osu file format v14
|
||||
|
||||
[Events]
|
||||
Video,-5678,"Video.avi",0,0
|
||||
F,0,1500,1600,0,1
|
||||
@@ -215,6 +215,35 @@ namespace osu.Game.Tests.Scores.IO
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreWithInvalidModCombinationsWillNotImport()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
|
||||
|
||||
var toImport = new ScoreInfo
|
||||
{
|
||||
User = new APIUser { Username = "Test user" },
|
||||
BeatmapInfo = beatmap.Beatmaps.First(),
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
ClientVersion = "12345",
|
||||
Mods = new Mod[] { new OsuModHalfTime(), new OsuModDoubleTime() },
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => LoadScoreIntoOsu(osu, toImport));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportStatistics()
|
||||
{
|
||||
|
||||
@@ -91,6 +91,6 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
}
|
||||
|
||||
private void assertCorrectIcon(bool favourited) => AddAssert("icon correct",
|
||||
() => this.ChildrenOfType<SpriteIcon>().Single().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart));
|
||||
() => this.ChildrenOfType<SpriteIcon>().First().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
{
|
||||
public partial class TestSceneBeatmapSetOnlineStatusPill : ThemeComparisonTestScene
|
||||
{
|
||||
private bool showUnknownStatus;
|
||||
|
||||
protected override Drawable CreateContent() => new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@@ -26,12 +28,20 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 10),
|
||||
ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast<BeatmapOnlineStatus>().Select(status => new BeatmapSetOnlineStatusPill
|
||||
ChildrenEnumerable = Enum.GetValues(typeof(BeatmapOnlineStatus)).Cast<BeatmapOnlineStatus>().Select(status => new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Status = status
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 20,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new BeatmapSetOnlineStatusPill
|
||||
{
|
||||
ShowUnknownStatus = showUnknownStatus,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Status = status
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -48,6 +58,12 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
pill.Width = 90;
|
||||
}));
|
||||
|
||||
AddStep("toggle show unknown", () =>
|
||||
{
|
||||
showUnknownStatus = !showUnknownStatus;
|
||||
CreateThemedContent(OverlayColourScheme.Red);
|
||||
});
|
||||
|
||||
AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
|
||||
}
|
||||
|
||||
@@ -65,11 +81,6 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
pill.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
break;
|
||||
|
||||
// skip none
|
||||
case BeatmapOnlineStatus.LocallyModified:
|
||||
pill.Status = BeatmapOnlineStatus.Graveyard;
|
||||
break;
|
||||
|
||||
default:
|
||||
pill.Status = (pill.Status + 1);
|
||||
break;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK;
|
||||
@@ -15,16 +13,18 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
{
|
||||
public partial class TestSceneDifficultySpectrumDisplay : OsuTestScene
|
||||
{
|
||||
private DifficultySpectrumDisplay display;
|
||||
private DifficultySpectrumDisplay display = null!;
|
||||
|
||||
private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
Beatmaps = difficulties.Select(difficulty => new APIBeatmap
|
||||
AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay
|
||||
{
|
||||
RulesetID = difficulty.rulesetId,
|
||||
StarRating = difficulty.stars
|
||||
}).ToArray()
|
||||
};
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(3)
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleRuleset()
|
||||
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
(rulesetId: 0, stars: 3.2),
|
||||
(rulesetId: 0, stars: 5.6));
|
||||
|
||||
createDisplay(beatmapSet);
|
||||
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
(rulesetId: 1, stars: 4.3),
|
||||
(rulesetId: 0, stars: 5.6));
|
||||
|
||||
createDisplay(beatmapSet);
|
||||
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -61,52 +61,30 @@ namespace osu.Game.Tests.Visual.Beatmaps
|
||||
(rulesetId: 0, stars: 5.6),
|
||||
(rulesetId: 15, stars: 7.8));
|
||||
|
||||
createDisplay(beatmapSet);
|
||||
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaximumUncollapsed()
|
||||
{
|
||||
var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 12).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray());
|
||||
createDisplay(beatmapSet);
|
||||
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMinimumCollapsed()
|
||||
{
|
||||
var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 13).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray());
|
||||
createDisplay(beatmapSet);
|
||||
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustableDotSize()
|
||||
private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet
|
||||
{
|
||||
var beatmapSet = createBeatmapSetWith(
|
||||
(rulesetId: 0, stars: 2.0),
|
||||
(rulesetId: 3, stars: 2.3),
|
||||
(rulesetId: 0, stars: 3.2),
|
||||
(rulesetId: 1, stars: 4.3),
|
||||
(rulesetId: 0, stars: 5.6));
|
||||
|
||||
createDisplay(beatmapSet);
|
||||
|
||||
AddStep("change dot dimensions", () =>
|
||||
Beatmaps = difficulties.Select(difficulty => new APIBeatmap
|
||||
{
|
||||
display.DotSize = new Vector2(8, 12);
|
||||
display.DotSpacing = 2;
|
||||
});
|
||||
AddStep("change dot dimensions back", () =>
|
||||
{
|
||||
display.DotSize = new Vector2(4, 8);
|
||||
display.DotSpacing = 1;
|
||||
});
|
||||
}
|
||||
|
||||
private void createDisplay(IBeatmapSetInfo beatmapSetInfo) => AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay(beatmapSetInfo)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(3)
|
||||
});
|
||||
RulesetID = difficulty.rulesetId,
|
||||
StarRating = difficulty.stars
|
||||
}).ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.SelectV2.Leaderboards;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.Metadata;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
@@ -43,7 +43,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -87,7 +85,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
AddStep("force transforms to finish", () => FinishTransforms(true));
|
||||
AddStep("right click second score", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<LeaderboardScoreV2>().ElementAt(1));
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapLeaderboardScore>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddAssert("use these mods not present",
|
||||
@@ -99,7 +97,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -114,7 +111,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
|
||||
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
@@ -128,7 +125,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -143,7 +139,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
|
||||
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
|
||||
@@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
[Test]
|
||||
public void TestDailyChallenge()
|
||||
{
|
||||
startChallenge(1234);
|
||||
startChallenge();
|
||||
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayIntroOnceFlag()
|
||||
{
|
||||
startChallenge(1234);
|
||||
startChallenge();
|
||||
AddStep("set intro played flag", () => Dependencies.Get<SessionStatics>().SetValue(Static.DailyChallengeIntroPlayed, true));
|
||||
|
||||
startChallenge(1235);
|
||||
startChallenge();
|
||||
|
||||
AddAssert("intro played flag reset", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.False);
|
||||
|
||||
@@ -62,13 +62,12 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
AddUntilStep("intro played flag set", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.True);
|
||||
}
|
||||
|
||||
private void startChallenge(int roomId)
|
||||
private void startChallenge()
|
||||
{
|
||||
AddStep("add room", () =>
|
||||
{
|
||||
API.Perform(new CreateRoomRequest(room = new Room
|
||||
{
|
||||
RoomID = roomId,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -83,7 +82,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
Category = RoomCategory.DailyChallenge
|
||||
}));
|
||||
});
|
||||
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }));
|
||||
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID!.Value }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull().ID)?.Value.DeletePending == true);
|
||||
|
||||
AddUntilStep("wait for default beatmap", () => Editor.Beatmap.Value is DummyWorkingBeatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -24,12 +24,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
PoolableSkinnableSample[] loopingSamples = null;
|
||||
PoolableSkinnableSample[] onceOffSamples = null;
|
||||
|
||||
AddStep("get first slider", () =>
|
||||
{
|
||||
slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
|
||||
onceOffSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => !s.Looping).ToArray();
|
||||
loopingSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => s.Looping).ToArray();
|
||||
});
|
||||
AddStep("get first slider", () => slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First());
|
||||
|
||||
AddStep("start playback", () => EditorClock.Start());
|
||||
|
||||
@@ -38,6 +33,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
if (!slider.Tracking.Value)
|
||||
return false;
|
||||
|
||||
onceOffSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => !s.Looping).ToArray();
|
||||
loopingSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => s.Looping).ToArray();
|
||||
|
||||
if (!loopingSamples.Any(s => s.Playing))
|
||||
return false;
|
||||
|
||||
|
||||
@@ -208,5 +208,11 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7));
|
||||
AddAssert("Correct beat divisor actually active", () => Editor.BeatDivisor, () => Is.EqualTo(7));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapVersionPopulatedCorrectly()
|
||||
{
|
||||
AddAssert("beatmap version is populated", () => EditorBeatmap.BeatmapVersion > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +65,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.Keys(PlatformAction.Paste);
|
||||
});
|
||||
|
||||
assertArtistMetadata("Example Artist");
|
||||
assertArtistMetadata("Example ArtistExample Artist");
|
||||
|
||||
// It's important values are committed immediately on focus loss so the editor exit sequence detects them.
|
||||
AddAssert("value immediately changed on focus loss", () =>
|
||||
AddAssert("value still changed after focus loss", () =>
|
||||
{
|
||||
((IFocusManager)InputManager).TriggerFocusContention(metadataSection);
|
||||
return editorBeatmap.Metadata.Artist;
|
||||
@@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.Keys(PlatformAction.Paste);
|
||||
});
|
||||
|
||||
assertArtistMetadata("Example Artist");
|
||||
assertArtistMetadata("Example ArtistExample Artist");
|
||||
|
||||
AddStep("commit", () => InputManager.Key(Key.Enter));
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -14,8 +15,10 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@@ -58,19 +61,63 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContextMenu()
|
||||
public void TestRightClickDuringEmptyPlacementTogglesNewCombo()
|
||||
{
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
|
||||
|
||||
AddStep("move mouse away from placed circle", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One));
|
||||
|
||||
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
|
||||
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
|
||||
AddAssert("new combo true", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.True));
|
||||
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
|
||||
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
|
||||
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
|
||||
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRightClickDuringPlacementDeletes()
|
||||
{
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
|
||||
|
||||
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(0).Items);
|
||||
AddAssert("circle not selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Exactly(0).Items);
|
||||
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRightClickDuringSelectionShowsContextMenu()
|
||||
{
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
|
||||
AddStep("delete with right mouse", () =>
|
||||
{
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items);
|
||||
// ensure the circle we're selecting is not a new combo so we can assert
|
||||
// new combo doesn't happen to get toggled by right click.
|
||||
AddStep("seek forward", () => EditorClock.Seek(1000));
|
||||
AddStep("place second circle", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("two circles added", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items);
|
||||
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
|
||||
AddStep("select selection tool", () => InputManager.Key(Key.Number1));
|
||||
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items);
|
||||
AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items);
|
||||
AddAssert("context menu visible", () => Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
|
||||
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -50,21 +50,17 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("Set short reference score", () =>
|
||||
{
|
||||
// 50 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
|
||||
List<HitEvent> hitEvents =
|
||||
[
|
||||
// 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
|
||||
new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
];
|
||||
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
hitEvents.Add(new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null));
|
||||
}
|
||||
|
||||
foreach (var ev in hitEvents)
|
||||
ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
|
||||
@@ -40,11 +40,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
breakTracker = new TestBreakTracker(),
|
||||
breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset()))
|
||||
breakOverlay = new BreakOverlay(new ScoreProcessor(new OsuRuleset()))
|
||||
{
|
||||
ProcessCustomClock = false,
|
||||
BreakTracker = breakTracker,
|
||||
}
|
||||
},
|
||||
new LetterboxOverlay
|
||||
{
|
||||
ProcessCustomClock = false,
|
||||
BreakTracker = breakTracker,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Screens.Play.Break;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneLetterboxOverlay : OsuTestScene
|
||||
{
|
||||
public TestSceneLetterboxOverlay()
|
||||
{
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new LetterboxOverlay()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
@@ -458,6 +459,62 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAnchorRadioButtonBehavior()
|
||||
{
|
||||
ISerialisableDrawable? selectedComponent = null;
|
||||
|
||||
AddStep("Select first component", () =>
|
||||
{
|
||||
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First();
|
||||
skinEditor.SelectedComponents.Clear();
|
||||
skinEditor.SelectedComponents.Add(blueprint.Item);
|
||||
selectedComponent = blueprint.Item;
|
||||
});
|
||||
|
||||
AddStep("Right-click to open context menu", () =>
|
||||
{
|
||||
if (selectedComponent != null)
|
||||
InputManager.MoveMouseTo(((Drawable)selectedComponent).ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddStep("Click on Anchor menu", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("Anchor"));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("Right-click TopLeft anchor", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("TopLeft"));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddAssert("TopLeft item checked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
|
||||
|
||||
AddStep("Right-click Centre anchor", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("Centre"));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddAssert("Centre item checked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
|
||||
AddAssert("TopLeft item unchecked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
|
||||
|
||||
AddStep("Right-click Closest anchor", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("Closest"));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddAssert("Closest item checked", () => (getMenuItemByText("Closest").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
|
||||
AddAssert("Centre item unchecked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
|
||||
|
||||
Menu.DrawableMenuItem getMenuItemByText(string text)
|
||||
=> this.ChildrenOfType<Menu.DrawableMenuItem>().First(m => m.Item.Text.ToString() == text);
|
||||
}
|
||||
|
||||
private Skin importSkinFromArchives(string filename)
|
||||
{
|
||||
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
|
||||
|
||||
@@ -75,8 +75,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching(
|
||||
spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5);
|
||||
|
||||
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
|
||||
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
|
||||
AddStep("change font to venera", () => list.HeaderFont.Value = Typeface.Venera);
|
||||
AddStep("change font to torus", () => list.HeaderFont.Value = Typeface.Torus);
|
||||
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
|
||||
|
||||
AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break);
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -48,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVideoSize()
|
||||
public void TestVideo()
|
||||
{
|
||||
AddStep("load storyboard with only video", () =>
|
||||
{
|
||||
@@ -56,6 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
loadStoryboard("storyboard_only_video.osu", s => s.Beatmap.WidescreenStoryboard = false);
|
||||
});
|
||||
|
||||
AddAssert("storyboard video present in hierarchy", () => this.ChildrenOfType<DrawableStoryboardVideo>().Any());
|
||||
AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f));
|
||||
}
|
||||
|
||||
|
||||
@@ -185,8 +185,12 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||
|
||||
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||
AddUntilStep("track changed", () =>
|
||||
AddUntilStep("new track selected", () =>
|
||||
trackChangeQueue.Count == 2 && !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||
|
||||
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||
AddUntilStep("first track selected",
|
||||
() => trackChangeQueue.Count == 3 && trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,30 +50,17 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
[Test]
|
||||
public void TestGameplay()
|
||||
{
|
||||
KiaiGameplayFountains fountains = null!;
|
||||
|
||||
AddStep("make fountains", () =>
|
||||
{
|
||||
Children = new[]
|
||||
{
|
||||
new KiaiGameplayFountains.GameplayStarFountain
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
X = 75,
|
||||
},
|
||||
new KiaiGameplayFountains.GameplayStarFountain
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
X = -75,
|
||||
},
|
||||
fountains = new KiaiGameplayFountains(),
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("activate fountains", () =>
|
||||
{
|
||||
((StarFountain)Children[0]).Shoot(1);
|
||||
((StarFountain)Children[1]).Shoot(-1);
|
||||
});
|
||||
AddStep("activate fountains", () => fountains.Shoot());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user