1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 02:09:52 +08:00

Compare commits

..

691 Commits

486 changed files with 13368 additions and 5762 deletions
+13 -3
View File
@@ -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
View File
@@ -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,
+58 -5
View File
@@ -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()
+11 -1
View File
@@ -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;
+75 -30
View File
@@ -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"))));
+8 -4
View File
@@ -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);
@@ -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}");
}
}
+1 -1
View File
@@ -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>
+11 -1
View File
@@ -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));
}
}
}
@@ -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