mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 23:12:56 +08:00
Merge branch 'master' into scorev3
This commit is contained in:
commit
4e9e084b2c
@ -21,7 +21,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2022.809.0",
|
||||
"version": "2023.712.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -339,6 +339,5 @@ inspectcode
|
||||
|
||||
# Fody (pulled in by Realm) - schema file
|
||||
FodyWeavers.xsd
|
||||
**/FodyWeavers.xml
|
||||
|
||||
.idea/.idea.osu.Desktop/.idea/misc.xml
|
@ -8,13 +8,9 @@
|
||||
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
|
||||
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.710.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.817.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
|
||||
<!-- for editor usage -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
</manifest>
|
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="mailto" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
@ -54,9 +54,6 @@ namespace osu.Desktop
|
||||
|
||||
client.OnReady += onReady;
|
||||
|
||||
// safety measure for now, until we performance test / improve backoff for failed connections.
|
||||
client.OnConnectionFailed += (_, _) => client.Deinitialize();
|
||||
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
|
||||
|
||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
||||
|
@ -75,7 +75,7 @@ namespace osu.Desktop.LegacyIpc
|
||||
case LegacyIpcDifficultyCalculationRequest req:
|
||||
try
|
||||
{
|
||||
WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile);
|
||||
WorkingBeatmap beatmap = new FlatWorkingBeatmap(req.BeatmapFile);
|
||||
var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
|
||||
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
|
||||
|
||||
|
@ -85,7 +85,7 @@ namespace osu.Desktop
|
||||
}
|
||||
}
|
||||
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true }))
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = !tournamentClient }))
|
||||
{
|
||||
if (!host.IsPrimaryInstance)
|
||||
{
|
||||
|
@ -26,7 +26,7 @@
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.1.4.20" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- using a different name because package name cannot contain 'catch' -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Catch_Tests.Android" android:installLocation="auto">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!catch Test" />
|
||||
</manifest>
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Rulesets.Catch.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Rulesets-Catch-Tests-iOS</string>
|
||||
<string>sh.ppy.catch-ruleset-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@ -42,4 +42,4 @@
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
@ -5,6 +5,7 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(catch_mod_mapping))]
|
||||
|
@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
@ -91,6 +93,9 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Relax))
|
||||
yield return new CatchModRelax();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||
@ -140,6 +145,12 @@ namespace osu.Game.Rulesets.Catch
|
||||
new CatchModNoScope(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
@ -209,5 +220,17 @@ namespace osu.Game.Rulesets.Catch
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
||||
|
||||
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
@ -24,7 +23,5 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
: base(new THitObject())
|
||||
{
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Mania.Tests.Android" android:installLocation="auto">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!mania Test" />
|
||||
</manifest>
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Rulesets.Mania.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Rulesets-Mania-Tests-iOS</string>
|
||||
<string>sh.ppy.mania-ruleset-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@ -42,4 +42,4 @@
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
@ -5,6 +5,7 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
@ -36,7 +37,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } },
|
||||
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
|
||||
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(mania_mod_mapping))]
|
||||
|
@ -6,7 +6,6 @@ using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Tests.Visual;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
@ -24,21 +23,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
Assert.False(testBeatmap.HitObjects.OfType<HoldNote>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectNoteValues()
|
||||
{
|
||||
var testBeatmap = createRawBeatmap();
|
||||
var noteValues = new List<double>(testBeatmap.HitObjects.OfType<HoldNote>().Count());
|
||||
|
||||
foreach (HoldNote h in testBeatmap.HitObjects.OfType<HoldNote>())
|
||||
{
|
||||
noteValues.Add(ManiaModHoldOff.GetNoteDurationInBeatLength(h, testBeatmap));
|
||||
}
|
||||
|
||||
noteValues.Sort();
|
||||
Assert.AreEqual(noteValues, new List<double> { 0.125, 0.250, 0.500, 1.000, 2.000 });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectObjectCount()
|
||||
{
|
||||
@ -47,25 +31,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
var rawBeatmap = createRawBeatmap();
|
||||
var testBeatmap = createModdedBeatmap();
|
||||
|
||||
// Calculate expected number of objects
|
||||
int expectedObjectCount = 0;
|
||||
|
||||
foreach (ManiaHitObject h in rawBeatmap.HitObjects)
|
||||
{
|
||||
// Both notes and hold notes account for at least one object
|
||||
expectedObjectCount++;
|
||||
|
||||
if (h.GetType() == typeof(HoldNote))
|
||||
{
|
||||
double noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, rawBeatmap);
|
||||
|
||||
if (noteValue >= ManiaModHoldOff.END_NOTE_ALLOW_THRESHOLD)
|
||||
{
|
||||
// Should generate an end note if it's longer than the minimum note value
|
||||
expectedObjectCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Both notes and hold notes account for at least one object
|
||||
int expectedObjectCount = rawBeatmap.HitObjects.Count;
|
||||
|
||||
Assert.That(testBeatmap.HitObjects.Count == expectedObjectCount);
|
||||
}
|
||||
|
147
osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs
Normal file
147
osu.Game.Rulesets.Mania.Tests/TestSceneMaximumScore.cs
Normal file
@ -0,0 +1,147 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneMaximumScore : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
private List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
|
||||
[Test]
|
||||
public void TestSimultaneousTickAndNote()
|
||||
{
|
||||
performTest(
|
||||
new List<ManiaHitObject>
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
Duration = 2000,
|
||||
Column = 0,
|
||||
},
|
||||
new Note
|
||||
{
|
||||
StartTime = 2000,
|
||||
Column = 1
|
||||
}
|
||||
},
|
||||
new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2),
|
||||
new ManiaReplayFrame(2001, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(3000)
|
||||
});
|
||||
|
||||
AddAssert("all objects perfectly judged",
|
||||
() => judgementResults.Select(result => result.Type),
|
||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSimultaneousLongNotes()
|
||||
{
|
||||
performTest(
|
||||
new List<ManiaHitObject>
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
Duration = 2000,
|
||||
Column = 0,
|
||||
},
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 2000,
|
||||
Duration = 2000,
|
||||
Column = 1
|
||||
}
|
||||
},
|
||||
new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000, ManiaAction.Key1, ManiaAction.Key2),
|
||||
new ManiaReplayFrame(3000, ManiaAction.Key2),
|
||||
new ManiaReplayFrame(4000)
|
||||
});
|
||||
|
||||
AddAssert("all objects perfectly judged",
|
||||
() => judgementResults.Select(result => result.Type),
|
||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||
}
|
||||
|
||||
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
|
||||
{
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects = hitObjects,
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -117,18 +117,16 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
private void createBarLine(bool major)
|
||||
{
|
||||
foreach (var stage in stages)
|
||||
var obj = new BarLine
|
||||
{
|
||||
var obj = new BarLine
|
||||
{
|
||||
StartTime = Time.Current + 2000,
|
||||
Major = major,
|
||||
};
|
||||
StartTime = Time.Current + 2000,
|
||||
Major = major,
|
||||
};
|
||||
|
||||
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
foreach (var stage in stages)
|
||||
stage.Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action)
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
{
|
||||
private const double individual_decay_base = 0.125;
|
||||
private const double overall_decay_base = 0.30;
|
||||
private const double release_threshold = 24;
|
||||
private const double release_threshold = 30;
|
||||
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 1;
|
||||
@ -50,10 +50,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
for (int i = 0; i < endTimes.Length; ++i)
|
||||
{
|
||||
// The current note is overlapped if a previous note or end is overlapping the current note body
|
||||
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1);
|
||||
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) &&
|
||||
Precision.DefinitelyBigger(endTime, endTimes[i], 1) &&
|
||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1);
|
||||
|
||||
// We give a slight bonus to everything if something is held meanwhile
|
||||
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1))
|
||||
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) &&
|
||||
Precision.DefinitelyBigger(startTime, startTimes[i], 1))
|
||||
holdFactor = 1.25;
|
||||
|
||||
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
|
||||
@ -70,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
// 0.0 +--------+-+---------------> Release Difference / ms
|
||||
// release_threshold
|
||||
if (isOverlapping)
|
||||
holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
|
||||
holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime)));
|
||||
|
||||
// Decay and increase individualStrains in own column
|
||||
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
|
||||
|
22
osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
Normal file
22
osu.Game.Rulesets.Mania/Edit/Setup/ManiaDifficultySection.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
public partial class ManiaDifficultySection : DifficultySection
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
CircleSizeSlider.Label = BeatmapsetsStrings.ShowStatsCsMania;
|
||||
CircleSizeSlider.Description = "The number of columns in the beatmap";
|
||||
if (CircleSizeSlider.Current is BindableNumber<float> circleSizeFloat)
|
||||
circleSizeFloat.Precision = 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Mirror))
|
||||
yield return new ManiaModMirror();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||
@ -285,6 +288,12 @@ namespace osu.Game.Rulesets.Mania
|
||||
new ModAdaptiveSpeed()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
@ -403,7 +412,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}, true),
|
||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
|
||||
{
|
||||
new AverageHitError(score.HitEvents),
|
||||
new UnstableRate(score.HitEvents)
|
||||
@ -416,6 +425,8 @@ namespace osu.Game.Rulesets.Mania
|
||||
}
|
||||
|
||||
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
|
||||
|
||||
public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
|
||||
}
|
||||
|
||||
public enum PlayfieldType
|
||||
|
@ -33,5 +33,6 @@ namespace osu.Game.Rulesets.Mania
|
||||
HitExplosion,
|
||||
StageBackground,
|
||||
StageForeground,
|
||||
BarLine
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) };
|
||||
|
||||
public const double END_NOTE_ALLOW_THRESHOLD = 0.5;
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||
@ -46,28 +44,9 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
StartTime = h.StartTime,
|
||||
Samples = h.GetNodeSamples(0)
|
||||
});
|
||||
|
||||
// Don't add an end note if the duration is shorter than the threshold
|
||||
double noteValue = GetNoteDurationInBeatLength(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc.
|
||||
|
||||
if (noteValue >= END_NOTE_ALLOW_THRESHOLD)
|
||||
{
|
||||
newObjects.Add(new Note
|
||||
{
|
||||
Column = h.Column,
|
||||
StartTime = h.EndTime,
|
||||
Samples = h.GetNodeSamples((h.NodeSamples?.Count - 1) ?? 1)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
|
||||
}
|
||||
|
||||
public static double GetNoteDurationInBeatLength(HoldNote holdNote, ManiaBeatmap beatmap)
|
||||
{
|
||||
double beatLength = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BeatLength;
|
||||
return holdNote.Duration / beatLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.Bindables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
@ -8,7 +9,15 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
public class BarLine : ManiaHitObject, IBarLine
|
||||
{
|
||||
public bool Major { get; set; }
|
||||
private HitObjectProperty<bool> major;
|
||||
|
||||
public Bindable<bool> MajorBindable => major.Bindable;
|
||||
|
||||
public bool Major
|
||||
{
|
||||
get => major.Value;
|
||||
set => major.Value = value;
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new IgnoreJudgement();
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
@ -13,45 +15,41 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// </summary>
|
||||
public partial class DrawableBarLine : DrawableManiaHitObject<BarLine>
|
||||
{
|
||||
public readonly Bindable<bool> Major = new Bindable<bool>();
|
||||
|
||||
public DrawableBarLine()
|
||||
: this(null!)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableBarLine(BarLine barLine)
|
||||
: base(barLine)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = barLine.Major ? 1.7f : 1.2f;
|
||||
}
|
||||
|
||||
AddInternal(new Box
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine())
|
||||
{
|
||||
Name = "Bar line",
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = barLine.Major ? 0.5f : 0.2f
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
if (barLine.Major)
|
||||
{
|
||||
Vector2 size = new Vector2(22, 6);
|
||||
const float line_offset = 4;
|
||||
Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true);
|
||||
}
|
||||
|
||||
AddInternal(new Circle
|
||||
{
|
||||
Name = "Left line",
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreRight,
|
||||
protected override void OnApply()
|
||||
{
|
||||
base.OnApply();
|
||||
Major.BindTo(HitObject.MajorBindable);
|
||||
}
|
||||
|
||||
Size = size,
|
||||
X = -line_offset,
|
||||
});
|
||||
|
||||
AddInternal(new Circle
|
||||
{
|
||||
Name = "Right line",
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = size,
|
||||
X = line_offset,
|
||||
});
|
||||
}
|
||||
protected override void OnFree()
|
||||
{
|
||||
base.OnFree();
|
||||
Major.UnbindFrom(HitObject.MajorBindable);
|
||||
}
|
||||
|
||||
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
|
||||
|
@ -2,7 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
@ -16,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateHitObjects(IBeatmap beatmap)
|
||||
=> base.EnumerateHitObjects(beatmap).OrderBy(ho => ho, JudgementOrderComparer.DEFAULT);
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 10000 * comboProgress
|
||||
@ -25,5 +33,29 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(400, combo_base));
|
||||
|
||||
private class JudgementOrderComparer : IComparer<HitObject>
|
||||
{
|
||||
public static readonly JudgementOrderComparer DEFAULT = new JudgementOrderComparer();
|
||||
|
||||
public int Compare(HitObject? x, HitObject? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y)) return 0;
|
||||
if (ReferenceEquals(x, null)) return -1;
|
||||
if (ReferenceEquals(y, null)) return 1;
|
||||
|
||||
int result = x.GetEndTime().CompareTo(y.GetEndTime());
|
||||
if (result != 0)
|
||||
return result;
|
||||
|
||||
// due to the way input is handled in mania, notes take precedence over ticks in judging order.
|
||||
if (x is Note && y is not Note) return -1;
|
||||
if (x is not Note && y is Note) return 1;
|
||||
|
||||
return x is ManiaHitObject maniaX && y is ManiaHitObject maniaY
|
||||
? maniaX.Column.CompareTo(maniaY.Column)
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
72
osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs
Normal file
72
osu.Game.Rulesets.Mania/Skinning/Default/DefaultBarLine.cs
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
{
|
||||
public partial class DefaultBarLine : CompositeDrawable
|
||||
{
|
||||
private Bindable<bool> major = null!;
|
||||
|
||||
private Drawable mainLine = null!;
|
||||
private Drawable leftAnchor = null!;
|
||||
private Drawable rightAnchor = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
AddInternal(mainLine = new Box
|
||||
{
|
||||
Name = "Bar line",
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
|
||||
Vector2 size = new Vector2(22, 6);
|
||||
const float line_offset = 4;
|
||||
|
||||
AddInternal(leftAnchor = new Circle
|
||||
{
|
||||
Name = "Left anchor",
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = size,
|
||||
X = -line_offset,
|
||||
});
|
||||
|
||||
AddInternal(rightAnchor = new Circle
|
||||
{
|
||||
Name = "Right anchor",
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = size,
|
||||
X = line_offset,
|
||||
});
|
||||
|
||||
major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
major.BindValueChanged(updateMajor, true);
|
||||
}
|
||||
|
||||
private void updateMajor(ValueChangedEvent<bool> major)
|
||||
{
|
||||
mainLine.Alpha = major.NewValue ? 0.5f : 0.2f;
|
||||
leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -119,6 +119,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
case ManiaSkinComponents.StageForeground:
|
||||
return new LegacyStageForeground();
|
||||
|
||||
case ManiaSkinComponents.BarLine:
|
||||
return null; // Not yet implemented.
|
||||
|
||||
default:
|
||||
throw new UnsupportedSkinComponentException(lookup);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -25,6 +26,22 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly List<Stage> stages = new List<Stage>();
|
||||
|
||||
public override Quad SkinnableComponentScreenSpaceDrawQuad
|
||||
{
|
||||
get
|
||||
{
|
||||
RectangleF totalArea = RectangleF.Empty;
|
||||
|
||||
for (int i = 0; i < Stages.Count; ++i)
|
||||
{
|
||||
var stageArea = Stages[i].ScreenSpaceDrawQuad.AABBFloat;
|
||||
totalArea = i == 0 ? stageArea : RectangleF.Union(totalArea, stageArea);
|
||||
}
|
||||
|
||||
return totalArea;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
|
||||
|
||||
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
|
||||
|
@ -136,6 +136,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
columnFlow.SetContentForColumn(i, column);
|
||||
AddNested(column);
|
||||
}
|
||||
|
||||
RegisterPool<BarLine, DrawableBarLine>(50, 200);
|
||||
}
|
||||
|
||||
private ISkinSource currentSkin;
|
||||
@ -186,7 +188,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
|
||||
|
||||
public void Add(BarLine barLine) => base.Add(new DrawableBarLine(barLine));
|
||||
public void Add(BarLine barLine) => base.Add(barLine);
|
||||
|
||||
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
{
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Osu.Tests.Android" android:installLocation="auto">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!standard Test" />
|
||||
</manifest>
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Rulesets.Osu.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Rulesets-Osu-Tests-iOS</string>
|
||||
<string>sh.ppy.osu-ruleset-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@ -42,4 +42,4 @@
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
@ -25,6 +25,35 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
[Test]
|
||||
public void TestSelectAfterFadedOut()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
|
||||
moveMouseToObject(() => slider);
|
||||
|
||||
AddStep("seek after end", () => EditorClock.Seek(750));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
|
||||
|
||||
AddStep("seek to visible", () => EditorClock.Seek(650));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContextMenuShownCorrectlyForSelectedSlider()
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
@ -70,12 +71,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
base.Content.Children = new Drawable[]
|
||||
{
|
||||
editorClock = new EditorClock(editorBeatmap),
|
||||
snapProvider,
|
||||
new PopoverContainer { Child = snapProvider },
|
||||
Content
|
||||
};
|
||||
}
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
|
@ -0,0 +1,95 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestScenePreciseRotation : TestSceneOsuEditor
|
||||
{
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset);
|
||||
|
||||
[Test]
|
||||
public void TestHotkeyHandling()
|
||||
{
|
||||
AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
|
||||
|
||||
AddStep("select first three objects", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Clear();
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Take(3));
|
||||
});
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.EqualTo(1));
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRotateCorrectness()
|
||||
{
|
||||
AddStep("replace objects", () =>
|
||||
{
|
||||
EditorBeatmap.Clear();
|
||||
EditorBeatmap.AddRange(new HitObject[]
|
||||
{
|
||||
new HitCircle { Position = new Vector2(100) },
|
||||
new HitCircle { Position = new Vector2(200) },
|
||||
});
|
||||
});
|
||||
AddStep("select both circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
|
||||
|
||||
AddStep("rotate by 180deg", () => getPopover().ChildrenOfType<TextBox>().Single().Current.Value = "180");
|
||||
AddAssert("first object rotated 180deg around playfield centre",
|
||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position,
|
||||
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(100)));
|
||||
AddAssert("second object rotated 180deg around playfield centre",
|
||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
|
||||
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
|
||||
|
||||
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(1).TriggerClick());
|
||||
AddAssert("first object rotated 90deg around selection centre",
|
||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
|
||||
AddAssert("second object rotated 90deg around selection centre",
|
||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position, () => Is.EqualTo(new Vector2(100, 100)));
|
||||
|
||||
PreciseRotationPopover? getPopover() => this.ChildrenOfType<PreciseRotationPopover>().SingleOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
@ -61,6 +61,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceWithMouseMovementOutsidePlayfield()
|
||||
{
|
||||
addMovementStep(new Vector2(200));
|
||||
addClickStep(MouseButton.Left);
|
||||
|
||||
AddStep("move mouse out of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + Vector2.One));
|
||||
addClickStep(MouseButton.Right);
|
||||
|
||||
assertPlaced(true);
|
||||
assertControlPointCount(2);
|
||||
assertControlPointType(0, PathType.Linear);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlaceNormalControlPoint()
|
||||
{
|
||||
|
110
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs
Normal file
110
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSliderReversal : TestSceneOsuEditor
|
||||
{
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
|
||||
|
||||
private readonly PathControlPoint[][] paths =
|
||||
{
|
||||
createPathSegment(
|
||||
PathType.PerfectCurve,
|
||||
new Vector2(200, -50),
|
||||
new Vector2(250, 0)
|
||||
),
|
||||
createPathSegment(
|
||||
PathType.Linear,
|
||||
new Vector2(100, 0),
|
||||
new Vector2(100, 100)
|
||||
)
|
||||
};
|
||||
|
||||
private static PathControlPoint[] createPathSegment(PathType type, params Vector2[] positions)
|
||||
{
|
||||
return positions.Select(p => new PathControlPoint
|
||||
{
|
||||
Position = p
|
||||
}).Prepend(new PathControlPoint
|
||||
{
|
||||
Type = type
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private Slider selectedSlider => (Slider)EditorBeatmap.SelectedHitObjects[0];
|
||||
|
||||
[TestCase(0, 250)]
|
||||
[TestCase(0, 200)]
|
||||
[TestCase(1, 120)]
|
||||
[TestCase(1, 80)]
|
||||
public void TestSliderReversal(int pathIndex, double length)
|
||||
{
|
||||
var controlPoints = paths[pathIndex];
|
||||
|
||||
Vector2 oldStartPos = default;
|
||||
Vector2 oldEndPos = default;
|
||||
double oldDistance = default;
|
||||
var oldControlPointTypes = controlPoints.Select(p => p.Type);
|
||||
|
||||
AddStep("Add slider", () =>
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
Position = new Vector2(OsuPlayfield.BASE_SIZE.X / 2, OsuPlayfield.BASE_SIZE.Y / 2),
|
||||
Path = new SliderPath(controlPoints)
|
||||
{
|
||||
ExpectedDistance = { Value = length }
|
||||
}
|
||||
};
|
||||
|
||||
EditorBeatmap.Add(slider);
|
||||
|
||||
oldStartPos = slider.Position;
|
||||
oldEndPos = slider.EndPosition;
|
||||
oldDistance = slider.Path.Distance;
|
||||
});
|
||||
|
||||
AddStep("Select slider", () =>
|
||||
{
|
||||
var slider = (Slider)EditorBeatmap.HitObjects[0];
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
});
|
||||
|
||||
AddStep("Reverse slider", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
|
||||
AddAssert("Slider has correct length", () =>
|
||||
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
|
||||
|
||||
AddAssert("Slider has correct start position", () =>
|
||||
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
|
||||
|
||||
AddAssert("Slider has correct end position", () =>
|
||||
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
|
||||
|
||||
AddAssert("Control points have correct types", () =>
|
||||
{
|
||||
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
|
||||
|
||||
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@ -28,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
|
||||
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
|
||||
new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(osu_mod_mapping))]
|
||||
|
147
osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs
Normal file
147
osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs
Normal file
@ -0,0 +1,147 @@
|
||||
// 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.Extensions.TypeExtensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneSpinnerJudgement : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private const double time_spinner_start = 2000;
|
||||
private const double time_spinner_end = 4000;
|
||||
|
||||
private List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
[Test]
|
||||
public void TestHitNothing()
|
||||
{
|
||||
performTest(new List<ReplayFrame>());
|
||||
|
||||
AddAssert("all min judgements", () => judgementResults.All(result => result.Type == result.Judgement.MinResult));
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
[TestCase(5)]
|
||||
public void TestNumberOfSpins(int spins)
|
||||
{
|
||||
performTest(generateReplay(spins));
|
||||
|
||||
for (int i = 0; i < spins; ++i)
|
||||
assertResult<SpinnerTick>(i, HitResult.SmallBonus);
|
||||
|
||||
assertResult<SpinnerTick>(spins, HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitEverything()
|
||||
{
|
||||
performTest(generateReplay(20));
|
||||
|
||||
AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult));
|
||||
}
|
||||
|
||||
private static List<ReplayFrame> generateReplay(int spins)
|
||||
{
|
||||
var replayFrames = new List<ReplayFrame>();
|
||||
|
||||
const int frames_per_spin = 30;
|
||||
|
||||
for (int i = 0; i < spins * frames_per_spin; ++i)
|
||||
{
|
||||
float totalProgress = i / (float)(spins * frames_per_spin);
|
||||
float spinProgress = (i % frames_per_spin) / (float)frames_per_spin;
|
||||
double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress;
|
||||
float posX = MathF.Cos(2 * MathF.PI * spinProgress);
|
||||
float posY = MathF.Sin(2 * MathF.PI * spinProgress);
|
||||
Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50;
|
||||
|
||||
replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton));
|
||||
}
|
||||
|
||||
return replayFrames;
|
||||
}
|
||||
|
||||
private void performTest(List<ReplayFrame> frames)
|
||||
{
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Spinner
|
||||
{
|
||||
StartTime = time_spinner_start,
|
||||
EndTime = time_spinner_end,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty(),
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
});
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private void assertResult<T>(int index, HitResult expectedResult)
|
||||
{
|
||||
AddAssert($"{typeof(T).ReadableName()} ({index}) judged as {expectedResult}",
|
||||
() => judgementResults.Where(j => j.HitObject is T).OrderBy(j => j.HitObject.StartTime).ElementAt(index).Type,
|
||||
() => Is.EqualTo(expectedResult));
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -214,17 +214,24 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
? currSlider.Position + currSlider.Path.PositionAt(1)
|
||||
: currHitObject.Position;
|
||||
|
||||
// Note the use of `StartTime` in the code below doesn't match stable's use of `EndTime`.
|
||||
// This is because in the stable implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
|
||||
// and therefore it does not have a correct `EndTime`, but instead the default of `EndTime = StartTime`.
|
||||
//
|
||||
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
||||
// if we use `EndTime` here it would result in unexpected stacking.
|
||||
|
||||
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
|
||||
{
|
||||
currHitObject.StackHeight++;
|
||||
startTime = beatmap.HitObjects[j].GetEndTime();
|
||||
startTime = beatmap.HitObjects[j].StartTime;
|
||||
}
|
||||
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance)
|
||||
{
|
||||
// Case for sliders - bump notes down and right, rather than up and left.
|
||||
sliderStack++;
|
||||
beatmap.HitObjects[j].StackHeight -= sliderStack;
|
||||
startTime = beatmap.HitObjects[j].GetEndTime();
|
||||
startTime = beatmap.HitObjects[j].StartTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
protected override bool AlwaysShowWhenSelected => true;
|
||||
|
||||
protected override bool ShouldBeAlive => base.ShouldBeAlive
|
||||
|| (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
|
||||
|| (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime
|
||||
&& editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
|
||||
|
||||
public override bool IsSelectable =>
|
||||
// Bypass fade out extension from hit markers for selection purposes.
|
||||
// This is to match stable, where even when the afterimage hit markers are still visible, objects are not selectable.
|
||||
base.ShouldBeAlive;
|
||||
|
||||
protected OsuSelectionBlueprint(T hitObject)
|
||||
: base(hitObject)
|
||||
|
@ -62,12 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private void load()
|
||||
{
|
||||
// Give a bit of breathing room around the playfield content.
|
||||
PlayfieldContentContainer.Padding = new MarginPadding
|
||||
{
|
||||
Vertical = 10,
|
||||
Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10,
|
||||
Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10,
|
||||
};
|
||||
PlayfieldContentContainer.Padding = new MarginPadding(10);
|
||||
|
||||
LayerBelowRuleset.AddRange(new Drawable[]
|
||||
{
|
||||
@ -90,6 +85,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
// we may be entering the screen with a selection already active
|
||||
updateDistanceSnapGrid();
|
||||
|
||||
RightToolbox.Add(new TransformToolboxGroup
|
||||
{
|
||||
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler
|
||||
});
|
||||
}
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -27,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial origin is stored so it can be used throughout the operation.
|
||||
/// </summary>
|
||||
private Vector2? referenceOrigin;
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial path types of a single selected slider are stored so they
|
||||
/// can be maintained throughout the operation.
|
||||
@ -42,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||
|
||||
SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
|
||||
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
|
||||
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||
@ -53,7 +48,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
protected override void OnOperationEnded()
|
||||
{
|
||||
base.OnOperationEnded();
|
||||
referenceOrigin = null;
|
||||
referencePathTypes = null;
|
||||
}
|
||||
|
||||
@ -109,13 +103,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects);
|
||||
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
bool didFlip = false;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position);
|
||||
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
|
||||
|
||||
if (!Precision.AlmostEquals(flippedPosition, h.Position))
|
||||
{
|
||||
@ -169,34 +163,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
public override bool HandleRotation(float delta)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = getSurroundingQuad(hitObjects);
|
||||
|
||||
referenceOrigin ??= quad.Centre;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
|
||||
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
foreach (PathControlPoint cp in path.Path.ControlPoints)
|
||||
cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta);
|
||||
}
|
||||
}
|
||||
|
||||
// this isn't always the case but let's be lenient for now.
|
||||
return true;
|
||||
}
|
||||
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale)
|
||||
{
|
||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
|
||||
|
||||
Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
||||
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
||||
|
||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
||||
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
|
||||
@ -222,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
slider.SnapTo(snapProvider);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
@ -238,10 +211,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
||||
{
|
||||
scale = getClampedScale(hitObjects, reference, scale);
|
||||
Quad selectionQuad = getSurroundingQuad(hitObjects);
|
||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
||||
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
||||
}
|
||||
|
||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||
@ -256,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = getSurroundingQuad(hitObjects);
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
Vector2 delta = Vector2.Zero;
|
||||
|
||||
@ -286,7 +259,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
||||
|
||||
Quad selectionQuad = getSurroundingQuad(hitObjects);
|
||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
|
||||
@ -311,26 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a gamefield-space quad surrounding the provided hit objects.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
|
||||
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
|
||||
GetSurroundingQuad(hitObjects.SelectMany(h =>
|
||||
{
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
h.Position,
|
||||
// can't use EndPosition for reverse slider cases.
|
||||
h.Position + path.Path.PositionAt(1)
|
||||
};
|
||||
}
|
||||
|
||||
return new[] { h.Position };
|
||||
}));
|
||||
|
||||
/// <summary>
|
||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||
/// </summary>
|
||||
|
107
osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
Normal file
107
osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
Normal file
@ -0,0 +1,107 @@
|
||||
// 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;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuSelectionRotationHandler : SelectionRotationHandler
|
||||
{
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedItems.CollectionChanged += (_, __) => updateState();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||
CanRotate.Value = quad.Width > 0 || quad.Height > 0;
|
||||
}
|
||||
|
||||
private OsuHitObject[]? objectsInRotation;
|
||||
|
||||
private Vector2? defaultOrigin;
|
||||
private Dictionary<OsuHitObject, Vector2>? originalPositions;
|
||||
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (objectsInRotation != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInRotation = selectedMovableObjects.ToArray();
|
||||
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
|
||||
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
|
||||
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
|
||||
obj => obj,
|
||||
obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray());
|
||||
}
|
||||
|
||||
public override void Update(float rotation, Vector2? origin = null)
|
||||
{
|
||||
if (objectsInRotation == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
|
||||
foreach (var ho in objectsInRotation)
|
||||
{
|
||||
ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation);
|
||||
|
||||
if (ho is IHasPath withPath)
|
||||
{
|
||||
var originalPath = originalPathControlPointPositions[withPath];
|
||||
|
||||
for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i)
|
||||
withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (objectsInRotation == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
objectsInRotation = null;
|
||||
originalPositions = null;
|
||||
originalPathControlPointPositions = null;
|
||||
defaultOrigin = null;
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
.Where(h => h is not Spinner);
|
||||
}
|
||||
}
|
107
osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs
Normal file
107
osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class PreciseRotationPopover : OsuPopover
|
||||
{
|
||||
private readonly SelectionRotationHandler rotationHandler;
|
||||
|
||||
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
|
||||
|
||||
private SliderWithTextBoxInput<float> angleInput = null!;
|
||||
private EditorRadioButtonCollection rotationOrigin = null!;
|
||||
|
||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
|
||||
{
|
||||
this.rotationHandler = rotationHandler;
|
||||
|
||||
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 220,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
angleInput = new SliderWithTextBoxInput<float>("Angle (degrees):")
|
||||
{
|
||||
Current = new BindableNumber<float>
|
||||
{
|
||||
MinValue = -360,
|
||||
MaxValue = 360,
|
||||
Precision = 1
|
||||
},
|
||||
Instantaneous = true
|
||||
},
|
||||
rotationOrigin = new EditorRadioButtonCollection
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = new[]
|
||||
{
|
||||
new RadioButton("Playfield centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||
new RadioButton("Selection centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ScheduleAfterChildren(() => angleInput.TakeFocus());
|
||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||
rotationOrigin.Items.First().Select();
|
||||
|
||||
rotationInfo.BindValueChanged(rotation =>
|
||||
{
|
||||
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
rotationHandler.Begin();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
|
||||
if (IsLoaded)
|
||||
rotationHandler.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
public enum RotationOrigin
|
||||
{
|
||||
PlayfieldCentre,
|
||||
SelectionCentre
|
||||
}
|
||||
|
||||
public record PreciseRotationInfo(float Degrees, RotationOrigin Origin);
|
||||
}
|
80
osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs
Normal file
80
osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private readonly Bindable<bool> canRotate = new BindableBool();
|
||||
|
||||
private EditorToolButton rotateButton = null!;
|
||||
|
||||
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
||||
|
||||
public TransformToolboxGroup()
|
||||
: base("transform")
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
rotateButton = new EditorToolButton("Rotate",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||
() => new PreciseRotationPopover(RotationHandler)),
|
||||
// TODO: scale
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// bindings to `Enabled` on the buttons are decoupled on purpose
|
||||
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
||||
canRotate.BindTo(RotationHandler.CanRotate);
|
||||
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat) return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.EditorToggleRotateControl:
|
||||
{
|
||||
rotateButton.TriggerClick();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -113,6 +113,9 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.TouchDevice))
|
||||
yield return new OsuModTouchDevice();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||
@ -212,6 +215,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
return new Mod[]
|
||||
{
|
||||
new OsuModTouchDevice(),
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
@ -315,7 +319,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}, true),
|
||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
|
||||
{
|
||||
new AverageHitError(timedHitEvents),
|
||||
new UnstableRate(timedHitEvents)
|
||||
|
@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
lastAngle = thisAngle;
|
||||
|
||||
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f;
|
||||
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
|
||||
|
||||
Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed));
|
||||
Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
texture.Bind();
|
||||
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
|
||||
drawPointQuad(renderer, points[i], textureRect, i + firstVisiblePointIndex);
|
||||
|
||||
UnbindTextureShader(renderer);
|
||||
renderer.PopLocalMatrix();
|
||||
@ -325,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
|
||||
|
||||
private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index)
|
||||
private void drawPointQuad(IRenderer renderer, SmokePoint point, RectangleF textureRect, int index)
|
||||
{
|
||||
Debug.Assert(quadBatch != null);
|
||||
|
||||
@ -347,25 +347,25 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
var localBotLeft = point.Position + ortho - dir;
|
||||
var localBotRight = point.Position + ortho + dir;
|
||||
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
quadBatch.Add(new TexturedVertex2D(renderer)
|
||||
{
|
||||
Position = localTopLeft,
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
|
||||
});
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
quadBatch.Add(new TexturedVertex2D(renderer)
|
||||
{
|
||||
Position = localTopRight,
|
||||
TexturePosition = textureRect.TopRight,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
|
||||
});
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
quadBatch.Add(new TexturedVertex2D(renderer)
|
||||
{
|
||||
Position = localBotRight,
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
|
||||
});
|
||||
quadBatch.Add(new TexturedVertex2D
|
||||
quadBatch.Add(new TexturedVertex2D(renderer)
|
||||
{
|
||||
Position = localBotLeft,
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Scoring;
|
||||
@ -120,18 +121,22 @@ namespace osu.Game.Rulesets.Osu.Statistics
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Overshoot",
|
||||
Font = OsuFont.GetFont(size: 12),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Padding = new MarginPadding(3),
|
||||
Origin = Anchor.BottomLeft,
|
||||
Padding = new MarginPadding(2),
|
||||
Rotation = -rotation,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Y = -(inner_portion + line_extension) / 2,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Undershoot",
|
||||
Font = OsuFont.GetFont(size: 12),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Padding = new MarginPadding(3),
|
||||
Origin = Anchor.TopRight,
|
||||
Rotation = -rotation,
|
||||
Padding = new MarginPadding(2),
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Y = (inner_portion + line_extension) / 2,
|
||||
},
|
||||
|
@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
if (time - part.Time >= 1)
|
||||
continue;
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
{
|
||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Time = part.Time
|
||||
});
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Time = part.Time
|
||||
});
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopRight,
|
||||
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Time = part.Time
|
||||
});
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||
{
|
||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
@ -362,12 +362,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
[VertexMember(1, VertexAttribPointerType.Float)]
|
||||
public float Time;
|
||||
|
||||
[VertexMember(1, VertexAttribPointerType.Int)]
|
||||
private readonly int maskingIndex;
|
||||
|
||||
public TexturedTrailVertex(IRenderer renderer)
|
||||
{
|
||||
this = default;
|
||||
maskingIndex = renderer.CurrentMaskingIndex;
|
||||
}
|
||||
|
||||
public bool Equals(TexturedTrailVertex other)
|
||||
{
|
||||
return Position.Equals(other.Position)
|
||||
&& TexturePosition.Equals(other.TexturePosition)
|
||||
&& Colour.Equals(other.Colour)
|
||||
&& Time.Equals(other.Time);
|
||||
&& Time.Equals(other.Time)
|
||||
&& maskingIndex == other.maskingIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Taiko.Tests.Android" android:installLocation="auto">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!taiko Test" />
|
||||
</manifest>
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Rulesets.Taiko.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Rulesets-Taiko-Tests-iOS</string>
|
||||
<string>sh.ppy.taiko-ruleset-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@ -42,4 +42,4 @@
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
@ -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 NUnit.Framework;
|
||||
@ -10,6 +11,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
@ -36,11 +38,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
() => Is.EqualTo(expectedResult));
|
||||
}
|
||||
|
||||
protected void PerformTest(List<ReplayFrame> frames, Beatmap<TaikoHitObject>? beatmap = null)
|
||||
protected void PerformTest(List<ReplayFrame> frames, Beatmap<TaikoHitObject>? beatmap = null, Mod[]? mods = null)
|
||||
{
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||
SelectedMods.Value = mods ?? Array.Empty<Mod>();
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
|
@ -75,6 +75,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitNoneStrongDrumRoll()
|
||||
{
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
}, CreateBeatmap(createDrumRoll(true)));
|
||||
|
||||
AssertJudgementCount(12);
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
AssertResult<DrumRollTick>(i, HitResult.IgnoreMiss);
|
||||
AssertResult<DrumRollTick.StrongNestedHit>(i, HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitAllStrongDrumRollWithOneKey()
|
||||
{
|
||||
|
@ -4,10 +4,14 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
{
|
||||
@ -32,6 +36,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
AssertResult<Hit>(0, HitResult.Great);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitWithBothKeysOnSameFrameDoesNotFallThroughToNextObject()
|
||||
{
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
}, CreateBeatmap(new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = 1000,
|
||||
}, new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = 1020
|
||||
}));
|
||||
|
||||
AssertJudgementCount(2);
|
||||
AssertResult<Hit>(0, HitResult.Great);
|
||||
AssertResult<Hit>(1, HitResult.Miss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitRimHit()
|
||||
{
|
||||
@ -157,5 +183,58 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
AssertJudgementCount(1);
|
||||
AssertResult<Hit>(0, HitResult.Ok);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStrongHitOneKeyWithHidden()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
var beatmap = CreateBeatmap(new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = hit_time,
|
||||
IsStrong = true
|
||||
});
|
||||
|
||||
var hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre),
|
||||
}, beatmap, new Mod[] { new TaikoModHidden() });
|
||||
|
||||
AssertJudgementCount(2);
|
||||
AssertResult<Hit>(0, HitResult.Ok);
|
||||
AssertResult<Hit.StrongNestedHit>(0, HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStrongHitTwoKeysWithHidden()
|
||||
{
|
||||
const double hit_time = 1000;
|
||||
|
||||
var beatmap = CreateBeatmap(new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = hit_time,
|
||||
IsStrong = true
|
||||
});
|
||||
|
||||
var hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
PerformTest(new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) - 1, TaikoAction.LeftCentre),
|
||||
new TaikoReplayFrame(hit_time + hitWindows.WindowFor(HitResult.Ok) + DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW - 2, TaikoAction.LeftCentre, TaikoAction.RightCentre),
|
||||
}, beatmap, new Mod[] { new TaikoModHidden() });
|
||||
|
||||
AssertJudgementCount(2);
|
||||
AssertResult<Hit>(0, HitResult.Ok);
|
||||
AssertResult<Hit.StrongNestedHit>(0, HitResult.LargeBonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,5 +114,75 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
||||
|
||||
AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure input is correctly sent to subsequent hits if a swell is fully completed.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitSwellThenHitHit()
|
||||
{
|
||||
const double swell_time = 1000;
|
||||
const double hit_time = 1150;
|
||||
|
||||
Swell swell = new Swell
|
||||
{
|
||||
StartTime = swell_time,
|
||||
Duration = 100,
|
||||
RequiredHits = 1
|
||||
};
|
||||
|
||||
Hit hit = new Hit
|
||||
{
|
||||
StartTime = hit_time
|
||||
};
|
||||
|
||||
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(swell_time, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
|
||||
};
|
||||
|
||||
PerformTest(frames, CreateBeatmap(swell, hit));
|
||||
|
||||
AssertJudgementCount(3);
|
||||
|
||||
AssertResult<SwellTick>(0, HitResult.IgnoreHit);
|
||||
AssertResult<Swell>(0, HitResult.LargeBonus);
|
||||
AssertResult<Hit>(0, HitResult.Great);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMissSwellThenHitHit()
|
||||
{
|
||||
const double swell_time = 1000;
|
||||
const double hit_time = 1150;
|
||||
|
||||
Swell swell = new Swell
|
||||
{
|
||||
StartTime = swell_time,
|
||||
Duration = 100,
|
||||
RequiredHits = 1
|
||||
};
|
||||
|
||||
Hit hit = new Hit
|
||||
{
|
||||
StartTime = hit_time
|
||||
};
|
||||
|
||||
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0),
|
||||
new TaikoReplayFrame(hit_time, TaikoAction.RightCentre),
|
||||
};
|
||||
|
||||
PerformTest(frames, CreateBeatmap(swell, hit));
|
||||
|
||||
AssertJudgementCount(3);
|
||||
|
||||
AssertResult<SwellTick>(0, HitResult.IgnoreMiss);
|
||||
AssertResult<Swell>(0, HitResult.IgnoreMiss);
|
||||
AssertResult<Hit>(0, HitResult.Great);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,71 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneTaikoModHidden : TaikoModTestScene
|
||||
{
|
||||
private Func<bool> checkAllMaxResultJudgements(int count) => ()
|
||||
=> Player.ScoreProcessor.JudgedHits >= count
|
||||
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
|
||||
|
||||
[Test]
|
||||
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModHidden(),
|
||||
Autoplay = true,
|
||||
PassCondition = checkSomeAutoplayHits
|
||||
PassCondition = checkAllMaxResultJudgements(4),
|
||||
});
|
||||
|
||||
private bool checkSomeAutoplayHits()
|
||||
=> Player.ScoreProcessor.JudgedHits >= 4
|
||||
&& Player.Results.All(result => result.Type == result.Judgement.MaxResult);
|
||||
[Test]
|
||||
public void TestHitTwoNotesWithinShortPeriod()
|
||||
{
|
||||
const double hit_time = 1;
|
||||
|
||||
var beatmap = new Beatmap<TaikoHitObject>
|
||||
{
|
||||
HitObjects = new List<TaikoHitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
Type = HitType.Rim,
|
||||
StartTime = hit_time,
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
Type = HitType.Centre,
|
||||
StartTime = hit_time * 2,
|
||||
},
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
SliderTickRate = 4,
|
||||
OverallDifficulty = 0,
|
||||
},
|
||||
Ruleset = new TaikoRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModHidden(),
|
||||
Autoplay = true,
|
||||
PassCondition = checkAllMaxResultJudgements(2),
|
||||
Beatmap = beatmap,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
|
||||
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(taiko_mod_mapping))]
|
||||
|
@ -139,7 +139,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
StartTime = obj.StartTime,
|
||||
Samples = obj.Samples,
|
||||
Duration = taikoDuration,
|
||||
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public partial class TaikoHitObjectComposer : HitObjectComposer<TaikoHitObject>
|
||||
{
|
||||
protected override bool ApplyHorizontalCentering => false;
|
||||
|
||||
public TaikoHitObjectComposer(TaikoRuleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
|
@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
hitObject.LifetimeEnd = state == ArmedState.Idle || !hitObject.AllJudged
|
||||
? hitObject.HitObject.GetEndTime() + hitObject.HitObject.HitWindows.WindowFor(HitResult.Miss)
|
||||
: hitObject.HitStateUpdateTime;
|
||||
// extend the lifetime end of the object in order to allow its nested strong hit (if any) to be judged.
|
||||
hitObject.LifetimeEnd += DrawableHit.StrongNestedHit.SECOND_HIT_WINDOW;
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -195,14 +195,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
public override void OnKilled()
|
||||
{
|
||||
base.OnKilled();
|
||||
|
||||
if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||
}
|
||||
}
|
||||
|
@ -108,14 +108,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
public override void OnKilled()
|
||||
{
|
||||
base.OnKilled();
|
||||
|
||||
if (Time.Current > ParentHitObject.HitObject.GetEndTime() && !Judged)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
private bool validActionPressed;
|
||||
|
||||
private bool pressHandledThisFrame;
|
||||
private double? lastPressHandleTime;
|
||||
|
||||
private readonly Bindable<HitType> type = new Bindable<HitType>();
|
||||
|
||||
@ -76,7 +76,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
HitActions = null;
|
||||
HitAction = null;
|
||||
validActionPressed = pressHandledThisFrame = false;
|
||||
validActionPressed = false;
|
||||
lastPressHandleTime = null;
|
||||
}
|
||||
|
||||
private void updateActionsFromType()
|
||||
@ -114,7 +115,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||
{
|
||||
if (pressHandledThisFrame)
|
||||
if (lastPressHandleTime == Time.Current)
|
||||
return true;
|
||||
if (Judged)
|
||||
return false;
|
||||
@ -128,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
|
||||
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
|
||||
pressHandledThisFrame = true;
|
||||
lastPressHandleTime = Time.Current;
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -139,15 +140,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
base.OnReleased(e);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// The input manager processes all input prior to us updating, so this is the perfect time
|
||||
// for us to remove the extra press blocking, before input is handled in the next frame
|
||||
pressHandledThisFrame = false;
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
{
|
||||
Debug.Assert(HitObject.HitWindows != null);
|
||||
|
@ -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.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Judgements;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
@ -16,5 +17,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
: base(nestedHit)
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnKilled()
|
||||
{
|
||||
base.OnKilled();
|
||||
|
||||
// usually, the strong nested hit isn't judged itself, it is judged by its parent object.
|
||||
// however, in rare cases (see: drum rolls, hits with hidden active),
|
||||
// it can happen that the hit window of the nested strong hit extends past the lifetime of the parent object.
|
||||
// this is a safety to prevent such cases from causing the nested hit to never be judged and as such prevent gameplay from completing.
|
||||
if (!Judged && Time.Current > ParentHitObject?.HitObject.GetEndTime())
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -276,6 +276,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (Time.Current < HitObject.StartTime)
|
||||
return false;
|
||||
|
||||
if (AllJudged)
|
||||
return false;
|
||||
|
||||
bool isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
|
||||
|
||||
// Ensure alternating centre and rim hits
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Threading;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
@ -14,7 +13,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity
|
||||
public class DrumRoll : TaikoStrongableHitObject, IHasPath
|
||||
{
|
||||
/// <summary>
|
||||
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
|
||||
@ -34,19 +33,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
/// </summary>
|
||||
public double Velocity { get; private set; }
|
||||
|
||||
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
Precision = 0.01,
|
||||
MinValue = 0.1,
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public double SliderVelocity
|
||||
{
|
||||
get => SliderVelocityBindable.Value;
|
||||
set => SliderVelocityBindable.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numer of ticks per beat length.
|
||||
/// </summary>
|
||||
@ -63,8 +49,9 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime);
|
||||
|
||||
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
|
||||
double scoringDistance = base_distance * difficulty.SliderMultiplier * effectPoint.ScrollSpeed;
|
||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||
|
||||
TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;
|
||||
|
@ -116,6 +116,9 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.Random))
|
||||
yield return new TaikoModRandom();
|
||||
|
||||
if (mods.HasFlagFast(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
}
|
||||
|
||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||
@ -176,6 +179,12 @@ namespace osu.Game.Rulesets.Taiko
|
||||
new ModAdaptiveSpeed()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
return Array.Empty<Mod>();
|
||||
}
|
||||
@ -247,7 +256,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250
|
||||
}, true),
|
||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||
new StatisticItem("Statistics", () => new SimpleStatisticTable(2, new SimpleStatisticItem[]
|
||||
{
|
||||
new AverageHitError(timedHitEvents),
|
||||
new UnstableRate(timedHitEvents)
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Tests.Android" android:installLocation="auto">
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
|
||||
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!visual Test" />
|
||||
</manifest>
|
@ -5,7 +5,7 @@
|
||||
<key>CFBundleName</key>
|
||||
<string>osu.Game.Tests.iOS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>ppy.osu-Game-Tests-iOS</string>
|
||||
<string>sh.ppy.osu-tests</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@ -42,4 +42,4 @@
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
@ -478,8 +478,8 @@ namespace osu.Game.Tests.Chat
|
||||
Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12"
|
||||
});
|
||||
|
||||
Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!\0\0\0", result.DisplayContent);
|
||||
Assert.AreEqual(5, result.Links.Count);
|
||||
Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now![emoji]", result.DisplayContent);
|
||||
Assert.AreEqual(4, result.Links.Count);
|
||||
|
||||
Link f = result.Links.Find(l => l.Url == "https://dev.ppy.sh/wiki/wiki links");
|
||||
Assert.That(f, Is.Not.Null);
|
||||
@ -500,27 +500,22 @@ namespace osu.Game.Tests.Chat
|
||||
Assert.That(f, Is.Not.Null);
|
||||
Assert.AreEqual(78, f.Index);
|
||||
Assert.AreEqual(18, f.Length);
|
||||
|
||||
f = result.Links.Find(l => l.Url == "\uD83D\uDE12");
|
||||
Assert.That(f, Is.Not.Null);
|
||||
Assert.AreEqual(101, f.Index);
|
||||
Assert.AreEqual(3, f.Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmoji()
|
||||
{
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" });
|
||||
Assert.AreEqual("Hello world\0\0\0<--This is an emoji,There are more:\0\0\0\0\0\0,\0\0\0", result.DisplayContent);
|
||||
Assert.AreEqual(result.Links.Count, 4);
|
||||
Assert.AreEqual(result.Links[0].Index, 11);
|
||||
Assert.AreEqual(result.Links[1].Index, 49);
|
||||
Assert.AreEqual(result.Links[2].Index, 52);
|
||||
Assert.AreEqual(result.Links[3].Index, 56);
|
||||
Assert.AreEqual(result.Links[0].Url, "\uD83D\uDE12");
|
||||
Assert.AreEqual(result.Links[1].Url, "\uD83D\uDE10");
|
||||
Assert.AreEqual(result.Links[2].Url, "\uD83D\uDE00");
|
||||
Assert.AreEqual(result.Links[3].Url, "\uD83D\uDE20");
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more emojis among us:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" });
|
||||
Assert.AreEqual("Hello world[emoji]<--This is an emoji,There are more emojis among us:[emoji][emoji],[emoji]", result.DisplayContent);
|
||||
Assert.AreEqual(result.Links.Count, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmojiWithSuccessiveParens()
|
||||
{
|
||||
Message result = MessageFormatter.FormatMessage(new Message { Content = "\uD83D\uDE10(let's hope this doesn't accidentally turn into a link)" });
|
||||
Assert.AreEqual("[emoji](let's hope this doesn't accidentally turn into a link)", result.DisplayContent);
|
||||
Assert.AreEqual(result.Links.Count, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -8,6 +8,9 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osu.Game.Tests.Visual;
|
||||
@ -15,7 +18,7 @@ using osu.Game.Tests.Visual;
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo
|
||||
public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo
|
||||
{
|
||||
public IBindable<bool> IsPlaying => isPlaying;
|
||||
|
||||
@ -59,7 +62,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
AddStep("Run background processor", () =>
|
||||
{
|
||||
Add(new TestBackgroundBeatmapProcessor());
|
||||
Add(new TestBackgroundDataStoreProcessor());
|
||||
});
|
||||
|
||||
AddUntilStep("wait for difficulties repopulated", () =>
|
||||
@ -98,7 +101,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
AddStep("Run background processor", () =>
|
||||
{
|
||||
Add(new TestBackgroundBeatmapProcessor());
|
||||
Add(new TestBackgroundDataStoreProcessor());
|
||||
});
|
||||
|
||||
AddWaitStep("wait some", 500);
|
||||
@ -124,7 +127,58 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
public partial class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor
|
||||
[Test]
|
||||
public void TestScoreUpgradeSuccess()
|
||||
{
|
||||
ScoreInfo scoreInfo = null!;
|
||||
|
||||
AddStep("Add score which requires upgrade (and has beatmap)", () =>
|
||||
{
|
||||
Realm.Write(r =>
|
||||
{
|
||||
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
|
||||
{
|
||||
TotalScoreVersion = 30000002,
|
||||
LegacyTotalScore = 123456,
|
||||
IsLegacyScore = true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
|
||||
|
||||
AddUntilStep("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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreUpgradeFailed()
|
||||
{
|
||||
ScoreInfo scoreInfo = null!;
|
||||
|
||||
AddStep("Add score which requires upgrade (but has no beatmap)", () =>
|
||||
{
|
||||
Realm.Write(r =>
|
||||
{
|
||||
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: new BeatmapInfo
|
||||
{
|
||||
BeatmapSet = new BeatmapSetInfo(),
|
||||
Ruleset = r.All<RulesetInfo>().First(),
|
||||
})
|
||||
{
|
||||
TotalScoreVersion = 30000002,
|
||||
IsLegacyScore = true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
|
||||
|
||||
AddUntilStep("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(30000002));
|
||||
}
|
||||
|
||||
public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
|
||||
{
|
||||
protected override int TimeToSleepDuringGameplay => 10;
|
||||
}
|
@ -1,19 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class LegacyBeatmapImporterTest
|
||||
public class LegacyBeatmapImporterTest : RealmTest
|
||||
{
|
||||
private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter();
|
||||
|
||||
@ -60,6 +64,33 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStableDateAddedApplied()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
|
||||
{
|
||||
var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
|
||||
var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);
|
||||
|
||||
ZipFile.ExtractToDirectory(TestResources.GetQuickTestBeatmapForImport(), songsStorage.GetFullPath("renatus"));
|
||||
|
||||
string[] beatmaps = Directory.GetFiles(songsStorage.GetFullPath("renatus"), "*.osu", SearchOption.TopDirectoryOnly);
|
||||
|
||||
File.SetLastWriteTimeUtc(beatmaps[beatmaps.Length / 2], new DateTime(2000, 1, 1, 12, 0, 0));
|
||||
|
||||
await new LegacyBeatmapImporter(new BeatmapImporter(storage, realm)).ImportFromStableAsync(stableStorage);
|
||||
|
||||
var importedSet = realm.Realm.All<BeatmapSetInfo>().Single();
|
||||
|
||||
Assert.NotNull(importedSet);
|
||||
Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Utc)), importedSet.DateAdded);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
|
||||
{
|
||||
public TestLegacyBeatmapImporter()
|
||||
|
183
osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs
Normal file
183
osu.Game.Tests/Editing/Checks/CheckBreaksTest.cs
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
public class CheckBreaksTest
|
||||
{
|
||||
private CheckBreaks check = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckBreaks();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBreakTooShort()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(0, 649)
|
||||
}
|
||||
};
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateTooShort);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBreakStartsEarly()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 0 },
|
||||
new HitCircle { StartTime = 1_200 }
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(100, 751)
|
||||
}
|
||||
};
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBreakEndsLate()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 0 },
|
||||
new HitCircle { StartTime = 1_298 }
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(200, 850)
|
||||
}
|
||||
};
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBreakAfterLastObjectStartsEarly()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 0 },
|
||||
new HitCircle { StartTime = 1200 }
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(1398, 2300)
|
||||
}
|
||||
};
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateEarlyStart);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBreakBeforeFirstObjectEndsLate()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 1100 },
|
||||
new HitCircle { StartTime = 1500 }
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(0, 652)
|
||||
}
|
||||
};
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBreakMultipleObjectsEarly()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 0 },
|
||||
new HitCircle { StartTime = 1_297 },
|
||||
new HitCircle { StartTime = 1_298 }
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(200, 850)
|
||||
}
|
||||
};
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBreaks.IssueTemplateLateEnd);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBreaksCorrect()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 0 },
|
||||
new HitCircle { StartTime = 1_300 }
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(200, 850)
|
||||
}
|
||||
};
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Is.Empty);
|
||||
}
|
||||
}
|
||||
}
|
85
osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs
Normal file
85
osu.Game.Tests/Editing/Checks/CheckDrainLengthTest.cs
Normal file
@ -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 System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
public class CheckDrainLengthTest
|
||||
{
|
||||
private CheckDrainLength check = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckDrainLength();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrainTimeShort()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 0 },
|
||||
new HitCircle { StartTime = 29_999 }
|
||||
}
|
||||
};
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrainTimeBreak()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 0 },
|
||||
new HitCircle { StartTime = 40_000 }
|
||||
},
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(10_000, 21_000)
|
||||
}
|
||||
};
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckDrainLength.IssueTemplateTooShort);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrainTimeCorrect()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i <= 30; ++i)
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i });
|
||||
|
||||
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
|
||||
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
|
||||
var issues = check.Run(context).ToList();
|
||||
|
||||
Assert.That(issues, Is.Empty);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@ -29,7 +30,7 @@ namespace osu.Game.Tests.Editing
|
||||
[Cached(typeof(IBeatSnapProvider))]
|
||||
private readonly EditorBeatmap editorBeatmap;
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
public TestSceneHitObjectComposerDistanceSnapping()
|
||||
{
|
||||
|
@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
|
||||
void main(void)
|
||||
{
|
||||
// Transform from screen space to masking space.
|
||||
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
|
||||
highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0);
|
||||
v_MaskingPosition = maskingPos.xy / maskingPos.z;
|
||||
|
||||
v_Colour = m_Colour;
|
||||
|
@ -125,13 +125,13 @@ namespace osu.Game.Tests.Visual.Background
|
||||
createFakeStoryboard();
|
||||
AddStep("Enable Storyboard", () =>
|
||||
{
|
||||
player.ReplacesBackground.Value = true;
|
||||
player.StoryboardReplacesBackground.Value = true;
|
||||
player.StoryboardEnabled.Value = true;
|
||||
});
|
||||
AddUntilStep("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is black, storyboard is visible", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack() && player.IsStoryboardVisible);
|
||||
AddStep("Disable Storyboard", () =>
|
||||
{
|
||||
player.ReplacesBackground.Value = false;
|
||||
player.StoryboardReplacesBackground.Value = false;
|
||||
player.StoryboardEnabled.Value = false;
|
||||
});
|
||||
AddUntilStep("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible);
|
||||
@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
createFakeStoryboard();
|
||||
AddStep("Enable Storyboard", () =>
|
||||
{
|
||||
player.ReplacesBackground.Value = true;
|
||||
player.StoryboardReplacesBackground.Value = true;
|
||||
player.StoryboardEnabled.Value = true;
|
||||
});
|
||||
AddStep("Enable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = false);
|
||||
@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
performFullSetup();
|
||||
createFakeStoryboard();
|
||||
AddStep("Enable replacing background", () => player.ReplacesBackground.Value = true);
|
||||
AddStep("Enable replacing background", () => player.StoryboardReplacesBackground.Value = true);
|
||||
|
||||
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
|
||||
@ -199,11 +199,11 @@ namespace osu.Game.Tests.Visual.Background
|
||||
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
|
||||
});
|
||||
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is invisible", () => songSelect.IsBackgroundInvisible());
|
||||
AddUntilStep("Background is dimmed", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack());
|
||||
|
||||
AddStep("Disable background replacement", () => player.ReplacesBackground.Value = false);
|
||||
AddStep("Disable background replacement", () => player.StoryboardReplacesBackground.Value = false);
|
||||
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
|
||||
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible() && !songSelect.IsBackgroundBlack());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
private void createFakeStoryboard() => AddStep("Create storyboard", () =>
|
||||
{
|
||||
player.StoryboardEnabled.Value = false;
|
||||
player.ReplacesBackground.Value = false;
|
||||
player.StoryboardReplacesBackground.Value = false;
|
||||
player.DimmableStoryboard.Add(new OsuSpriteText
|
||||
{
|
||||
Size = new Vector2(500, 50),
|
||||
@ -323,6 +323,8 @@ namespace osu.Game.Tests.Visual.Background
|
||||
config.BindWith(OsuSetting.BlurLevel, BlurLevel);
|
||||
}
|
||||
|
||||
public bool IsBackgroundBlack() => background.CurrentColour == OsuColour.Gray(0);
|
||||
|
||||
public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim);
|
||||
|
||||
public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White;
|
||||
@ -331,8 +333,6 @@ namespace osu.Game.Tests.Visual.Background
|
||||
|
||||
public bool IsUserBlurDisabled() => background.CurrentBlur == new Vector2(0);
|
||||
|
||||
public bool IsBackgroundInvisible() => background.CurrentAlpha == 0;
|
||||
|
||||
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
|
||||
|
||||
public bool IsBackgroundBlur() => Precision.AlmostEquals(background.CurrentBlur, new Vector2(BACKGROUND_BLUR), 0.1f);
|
||||
@ -367,7 +367,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
base.OnEntering(e);
|
||||
|
||||
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
|
||||
ApplyToBackground(b => StoryboardReplacesBackground.BindTo(b.StoryboardReplacesBackground));
|
||||
}
|
||||
|
||||
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
|
||||
@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
public bool BlockLoad;
|
||||
|
||||
public Bindable<bool> StoryboardEnabled;
|
||||
public readonly Bindable<bool> ReplacesBackground = new Bindable<bool>();
|
||||
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
|
||||
public readonly Bindable<bool> IsPaused = new Bindable<bool>();
|
||||
|
||||
public LoadBlockingTestPlayer(bool allowPause = true)
|
||||
|
@ -22,7 +22,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
|
||||
{
|
||||
private BeatDivisorControl beatDivisorControl = null!;
|
||||
private BindableBeatDivisor bindableBeatDivisor = null!;
|
||||
|
||||
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
|
||||
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single();
|
||||
@ -30,13 +29,19 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[Cached]
|
||||
private readonly BindableBeatDivisor bindableBeatDivisor = new BindableBeatDivisor(16);
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
bindableBeatDivisor.ValidDivisors.SetDefault();
|
||||
bindableBeatDivisor.SetDefault();
|
||||
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16))
|
||||
Child = beatDivisorControl = new BeatDivisorControl
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -3,8 +3,11 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
@ -20,6 +23,14 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private Container selectionArea;
|
||||
private SelectionBox selectionBox;
|
||||
|
||||
[Cached(typeof(SelectionRotationHandler))]
|
||||
private TestSelectionRotationHandler rotationHandler;
|
||||
|
||||
public TestSceneComposeSelectBox()
|
||||
{
|
||||
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
@ -34,13 +45,11 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
CanRotate = true,
|
||||
CanScaleX = true,
|
||||
CanScaleY = true,
|
||||
CanFlipX = true,
|
||||
CanFlipY = true,
|
||||
|
||||
OnRotation = handleRotation,
|
||||
OnScale = handleScale
|
||||
}
|
||||
}
|
||||
@ -71,11 +80,48 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool handleRotation(float angle)
|
||||
private partial class TestSelectionRotationHandler : SelectionRotationHandler
|
||||
{
|
||||
// kinda silly and wrong, but just showing that the drag handles work.
|
||||
selectionArea.Rotation += angle;
|
||||
return true;
|
||||
private readonly Func<Container> getTargetContainer;
|
||||
|
||||
public TestSelectionRotationHandler(Func<Container> getTargetContainer)
|
||||
{
|
||||
this.getTargetContainer = getTargetContainer;
|
||||
|
||||
CanRotate.Value = true;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private Container targetContainer;
|
||||
|
||||
private float? initialRotation;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (targetContainer != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
|
||||
|
||||
targetContainer = getTargetContainer();
|
||||
initialRotation = targetContainer!.Rotation;
|
||||
}
|
||||
|
||||
public override void Update(float rotation, Vector2? origin = null)
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
// kinda silly and wrong, but just showing that the drag handles work.
|
||||
targetContainer.Rotation = initialRotation!.Value + rotation;
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
targetContainer = null;
|
||||
initialRotation = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -1,26 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -82,7 +80,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestNudgeSelection()
|
||||
{
|
||||
HitCircle[] addedObjects = null;
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
@ -104,7 +102,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestRotateHotkeys()
|
||||
{
|
||||
HitCircle[] addedObjects = null;
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
@ -136,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestGlobalFlipHotkeys()
|
||||
{
|
||||
HitCircle addedObject = null;
|
||||
HitCircle addedObject = null!;
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.Add(addedObject = new HitCircle { StartTime = 100 }));
|
||||
|
||||
@ -217,11 +215,173 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNearestSelection()
|
||||
{
|
||||
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
|
||||
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 };
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject }));
|
||||
|
||||
moveMouseToObject(() => firstObject);
|
||||
|
||||
AddStep("seek near first", () => EditorClock.Seek(100));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
|
||||
|
||||
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||
|
||||
AddStep("seek near second", () => EditorClock.Seek(500));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
|
||||
|
||||
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||
|
||||
AddStep("seek halfway", () => EditorClock.Seek(300));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNearestSelectionWithEndTime()
|
||||
{
|
||||
var firstObject = new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 0,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(50, 0)),
|
||||
})
|
||||
};
|
||||
|
||||
var secondObject = new HitCircle
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
StartTime = 600
|
||||
};
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new HitObject[] { firstObject, secondObject }));
|
||||
|
||||
moveMouseToObject(() => firstObject);
|
||||
|
||||
AddStep("seek near first", () => EditorClock.Seek(100));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
|
||||
|
||||
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||
|
||||
AddStep("seek near second", () => EditorClock.Seek(500));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
|
||||
|
||||
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||
|
||||
AddStep("seek roughly halfway", () => EditorClock.Seek(350));
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
// Slider gets priority due to end time.
|
||||
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCyclicSelection()
|
||||
{
|
||||
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
|
||||
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 };
|
||||
var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 };
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject }));
|
||||
|
||||
moveMouseToObject(() => firstObject);
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject));
|
||||
|
||||
// cycle around
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCyclicSelectionOutwards()
|
||||
{
|
||||
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
|
||||
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 };
|
||||
var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 };
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject }));
|
||||
|
||||
moveMouseToObject(() => firstObject);
|
||||
|
||||
AddStep("seek near second", () => EditorClock.Seek(320));
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject));
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
|
||||
|
||||
// cycle around
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCyclicSelectionBackwards()
|
||||
{
|
||||
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
|
||||
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 200 };
|
||||
var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 400 };
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject }));
|
||||
|
||||
moveMouseToObject(() => firstObject);
|
||||
|
||||
AddStep("seek to third", () => EditorClock.Seek(350));
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject));
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
|
||||
|
||||
// cycle around
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDoubleClickToSeek()
|
||||
{
|
||||
var hitCircle = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 };
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { hitCircle }));
|
||||
|
||||
moveMouseToObject(() => hitCircle);
|
||||
|
||||
AddRepeatStep("double click", () => InputManager.Click(MouseButton.Left), 2);
|
||||
|
||||
AddUntilStep("seeked to circle", () => EditorClock.CurrentTime, () => Is.EqualTo(600));
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag)
|
||||
{
|
||||
HitCircle[] addedObjects = null;
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
@ -320,7 +480,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestQuickDeleteRemovesSliderControlPoint()
|
||||
{
|
||||
Slider slider = null;
|
||||
Slider slider = null!;
|
||||
|
||||
PathControlPoint[] points =
|
||||
{
|
||||
|
@ -18,6 +18,7 @@ using osu.Game.Database;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
@ -91,25 +92,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
/*
|
||||
* Fail rate around 1.2%.
|
||||
*
|
||||
* Failing with realm refetch occasionally being null.
|
||||
* My only guess is that the WorkingBeatmap at SetupScreen is dummy instead of the true one.
|
||||
* If it's something else, we have larger issues with realm, but I don't think that's the case.
|
||||
*
|
||||
* at osu.Framework.Logging.ThrowingTraceListener.Fail(String message1, String message2)
|
||||
* at System.Diagnostics.TraceInternal.Fail(String message, String detailMessage)
|
||||
* at System.Diagnostics.TraceInternal.TraceProvider.Fail(String message, String detailMessage)
|
||||
* at System.Diagnostics.Debug.Fail(String message, String detailMessage)
|
||||
* at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.<performFileOperation>b__0(Realm realm) ModelManager.cs:line 50
|
||||
* at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) RealmExtensions.cs:line 14
|
||||
* at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) ModelManager.cs:line 47
|
||||
* at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) ModelManager.cs:line 37
|
||||
* at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) ResourcesSection.cs:line 115
|
||||
* at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.<TestAddAudioTrack>b__11_0() TestSceneEditorBeatmapCreation.cs:line 101
|
||||
*/
|
||||
public void TestAddAudioTrack()
|
||||
{
|
||||
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
|
||||
@ -453,6 +435,51 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExitBlockedWhenSavingBeatmapWithSameNamedDifficulties()
|
||||
{
|
||||
Guid setId = Guid.Empty;
|
||||
const string duplicate_difficulty_name = "duplicate";
|
||||
|
||||
AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID);
|
||||
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
|
||||
AddStep("save beatmap", () => Editor.Save());
|
||||
AddAssert("new beatmap persisted", () =>
|
||||
{
|
||||
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
|
||||
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
|
||||
});
|
||||
|
||||
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo));
|
||||
|
||||
AddUntilStep("wait for created", () =>
|
||||
{
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName != duplicate_difficulty_name;
|
||||
});
|
||||
AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded);
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
|
||||
{
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 0
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 1000
|
||||
}
|
||||
}));
|
||||
|
||||
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
|
||||
AddUntilStep("wait for has unsaved changes", () => Editor.HasUnsavedChanges);
|
||||
|
||||
AddStep("exit", () => Editor.Exit());
|
||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is PromptForSaveDialog);
|
||||
AddStep("attempt to save", () => DialogOverlay.CurrentDialog.PerformOkAction());
|
||||
AddAssert("editor is still current", () => Editor.IsCurrentScreen());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCreateNewDifficultyForInconvertibleRuleset()
|
||||
{
|
||||
|
@ -8,7 +8,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@ -117,9 +117,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("move mouse to overlapping toggle button", () =>
|
||||
{
|
||||
var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad;
|
||||
var button = toolboxContainer.ChildrenOfType<DrawableTernaryButton>().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
|
||||
var button = toolboxContainer.ChildrenOfType<DrawableTernaryButton>().First(b => playfield.Contains(getOverlapPoint(b)));
|
||||
|
||||
InputManager.MoveMouseTo(button);
|
||||
InputManager.MoveMouseTo(getOverlapPoint(button));
|
||||
});
|
||||
|
||||
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
|
||||
@ -127,6 +127,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("attempt place circle", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
|
||||
|
||||
Vector2 getOverlapPoint(DrawableTernaryButton ternaryButton)
|
||||
{
|
||||
var quad = ternaryButton.ScreenSpaceDrawQuad;
|
||||
return quad.TopLeft + new Vector2(quad.Width * 9 / 10, quad.Height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -172,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
|
||||
}
|
||||
|
||||
public partial class EditorBeatmapContainer : Container
|
||||
public partial class EditorBeatmapContainer : PopoverContainer
|
||||
{
|
||||
private readonly IWorkingBeatmap working;
|
||||
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
BeatDivisor.Value = 4;
|
||||
|
||||
Add(new BeatDivisorControl(BeatDivisor)
|
||||
Add(new BeatDivisorControl
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private HUDOverlay hudOverlay = null!;
|
||||
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
|
||||
private ScoreProcessor scoreProcessor { get; set; }
|
||||
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
@ -47,6 +47,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
private Drawable keyCounterFlow => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
|
||||
|
||||
public TestSceneHUDOverlay()
|
||||
{
|
||||
scoreProcessor = gameplayState.ScoreProcessor;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
@ -69,13 +69,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
spewer.Clock = new FramedClock(testClock);
|
||||
});
|
||||
AddStep("start spewer", () => spewer.Active.Value = true);
|
||||
AddAssert("spawned first particle", () => spewer.TotalCreatedParticles == 1);
|
||||
AddAssert("spawned first particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3);
|
||||
AddAssert("spawned second particle", () => spewer.TotalCreatedParticles == 2);
|
||||
AddAssert("spawned second particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(2));
|
||||
|
||||
AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1);
|
||||
AddAssert("spawned third particle", () => spewer.TotalCreatedParticles == 3);
|
||||
AddAssert("spawned third particle", () => spewer.TotalCreatedParticles, () => Is.EqualTo(3));
|
||||
}
|
||||
|
||||
private TestParticleSpewer createSpewer() =>
|
||||
|
@ -21,9 +21,11 @@ using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -147,6 +149,38 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReplayExport()
|
||||
{
|
||||
CreateTest();
|
||||
|
||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||
|
||||
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
|
||||
|
||||
AddUntilStep("results displayed", () => (Player.GetChildScreen() as ResultsScreen)?.IsLoaded == true);
|
||||
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
|
||||
|
||||
AddUntilStep("wait for button clickable", () => ((OsuScreen)Player.GetChildScreen())
|
||||
.ChildrenOfType<ReplayDownloadButton>().FirstOrDefault()?
|
||||
.ChildrenOfType<OsuClickableContainer>().FirstOrDefault()?
|
||||
.Enabled.Value == true);
|
||||
|
||||
AddAssert("no export files", () => !LocalStorage.GetFiles("exports").Any());
|
||||
|
||||
AddStep("Export replay", () => InputManager.PressKey(Key.F2));
|
||||
|
||||
string? filePath = null;
|
||||
|
||||
// Files starting with _ are temporary, created by CreateFileSafely call.
|
||||
AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !f.StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null);
|
||||
AddAssert("filesize is non-zero", () =>
|
||||
{
|
||||
using (var stream = LocalStorage.GetStream(filePath))
|
||||
return stream.Length;
|
||||
}, () => Is.Not.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreStoredLocallyCustomRuleset()
|
||||
{
|
||||
|
@ -138,24 +138,28 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestCyclicSelection()
|
||||
{
|
||||
SkinBlueprint[] blueprints = null!;
|
||||
List<SkinBlueprint> blueprints = new List<SkinBlueprint>();
|
||||
|
||||
AddStep("Add big black boxes", () =>
|
||||
AddStep("clear list", () => blueprints.Clear());
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("Add big black box", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("store box", () =>
|
||||
{
|
||||
// Add blueprints one-by-one so we have a stable order for testing reverse cyclic selection against.
|
||||
blueprints.Add(skinEditor.ChildrenOfType<SkinBlueprint>().Single(s => s.IsSelected));
|
||||
});
|
||||
}
|
||||
|
||||
AddAssert("Three black boxes added", () => targetContainer.Components.OfType<BigBlackBox>().Count(), () => Is.EqualTo(3));
|
||||
|
||||
AddStep("Store black box blueprints", () =>
|
||||
{
|
||||
blueprints = skinEditor.ChildrenOfType<SkinBlueprint>().Where(b => b.Item is BigBlackBox).ToArray();
|
||||
});
|
||||
|
||||
AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
|
||||
AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item));
|
||||
|
||||
AddStep("move cursor to black box", () =>
|
||||
{
|
||||
@ -164,13 +168,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
|
||||
AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("Selection is black box 2", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item));
|
||||
AddAssert("Selection is second last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item));
|
||||
|
||||
AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("Selection is black box 3", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item));
|
||||
AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
|
||||
|
||||
AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
|
||||
AddAssert("Selection is first", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item));
|
||||
|
||||
AddStep("select all boxes", () =>
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -19,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestToggleEditor()
|
||||
{
|
||||
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox
|
||||
var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect));
|
||||
|
||||
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null)
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public partial class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
|
||||
{
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
|
||||
private ScoreProcessor scoreProcessor { get; set; }
|
||||
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
@ -37,6 +37,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached]
|
||||
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
||||
|
||||
public TestSceneSkinEditorMultipleSkins()
|
||||
{
|
||||
scoreProcessor = gameplayState.ScoreProcessor;
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private HUDOverlay hudOverlay;
|
||||
|
||||
[Cached(typeof(ScoreProcessor))]
|
||||
private ScoreProcessor scoreProcessor => gameplayState.ScoreProcessor;
|
||||
private ScoreProcessor scoreProcessor { get; set; }
|
||||
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
@ -47,6 +47,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
private Drawable keyCounterFlow => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
|
||||
|
||||
public TestSceneSkinnableHUDOverlay()
|
||||
{
|
||||
scoreProcessor = gameplayState.ScoreProcessor;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestComboCounterIncrementing()
|
||||
{
|
||||
|
@ -185,6 +185,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGetSegmentEnds()
|
||||
{
|
||||
var positions = new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(100, 0),
|
||||
new Vector2(100),
|
||||
new Vector2(200, 100),
|
||||
};
|
||||
double[] distances = { 100d, 200d, 300d };
|
||||
|
||||
AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear))));
|
||||
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 300)));
|
||||
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
|
||||
|
||||
AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 400);
|
||||
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 400)));
|
||||
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
|
||||
|
||||
AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
|
||||
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 150)));
|
||||
// see remarks in `GetSegmentEnds()` xmldoc (`SliderPath.PositionAt()` clamps progress to [0,1]).
|
||||
AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(new[]
|
||||
{
|
||||
positions[1],
|
||||
new Vector2(100, 50),
|
||||
new Vector2(100, 50),
|
||||
}));
|
||||
}
|
||||
|
||||
private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints)
|
||||
{
|
||||
var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();
|
||||
|
@ -20,7 +20,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Beatmap,
|
||||
Mod = new UnknownMod("WNG"),
|
||||
PassCondition = () => Player.IsLoaded && !Player.LoadedBeatmapSuccessfully
|
||||
});
|
||||
|
52
osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs
Normal file
52
osu.Game.Tests/Visual/Menus/TestSceneStarFountain.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneStarFountain : OsuTestScene
|
||||
{
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("make fountains", () =>
|
||||
{
|
||||
Children = new[]
|
||||
{
|
||||
new StarFountain
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
X = 200,
|
||||
},
|
||||
new StarFountain
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
X = -200,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPew()
|
||||
{
|
||||
AddRepeatStep("activate fountains sometimes", () =>
|
||||
{
|
||||
foreach (var fountain in Children.OfType<StarFountain>())
|
||||
{
|
||||
if (RNG.NextSingle() > 0.8f)
|
||||
fountain.Shoot(RNG.Next(-1, 2));
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
}
|
@ -84,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFocusOnTabKeyWhenExpanded()
|
||||
public void TestFocusOnEnterKeyWhenExpanded()
|
||||
{
|
||||
setLocalUserPlaying(true);
|
||||
|
||||
assertChatFocused(false);
|
||||
AddStep("press tab", () => InputManager.Key(Key.Tab));
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
assertChatFocused(true);
|
||||
}
|
||||
|
||||
@ -99,19 +99,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
setLocalUserPlaying(true);
|
||||
|
||||
assertChatFocused(false);
|
||||
AddStep("press tab", () => InputManager.Key(Key.Tab));
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
assertChatFocused(true);
|
||||
AddStep("press escape", () => InputManager.Key(Key.Escape));
|
||||
assertChatFocused(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFocusOnTabKeyWhenNotExpanded()
|
||||
public void TestFocusOnEnterKeyWhenNotExpanded()
|
||||
{
|
||||
AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
|
||||
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
|
||||
|
||||
AddStep("press tab", () => InputManager.Key(Key.Tab));
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
assertChatFocused(true);
|
||||
AddUntilStep("is visible", () => chatDisplay.IsPresent);
|
||||
|
||||
@ -120,21 +120,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFocusToggleViaAction()
|
||||
{
|
||||
AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
|
||||
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
|
||||
|
||||
AddStep("press tab", () => InputManager.Key(Key.Tab));
|
||||
assertChatFocused(true);
|
||||
AddUntilStep("is visible", () => chatDisplay.IsPresent);
|
||||
|
||||
AddStep("press tab", () => InputManager.Key(Key.Tab));
|
||||
assertChatFocused(false);
|
||||
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
|
||||
}
|
||||
|
||||
private void assertChatFocused(bool isFocused) =>
|
||||
AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused);
|
||||
|
||||
|
@ -49,6 +49,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Expanded = { Value = true }
|
||||
}, Add);
|
||||
});
|
||||
|
@ -65,6 +65,47 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("clear playing users", () => playingUsers.Clear());
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(4)]
|
||||
[TestCase(9)]
|
||||
public void TestGeneral(int count)
|
||||
{
|
||||
int[] userIds = getPlayerIds(count);
|
||||
|
||||
start(userIds);
|
||||
loadSpectateScreen();
|
||||
|
||||
sendFrames(userIds, 1000);
|
||||
AddWaitStep("wait a bit", 20);
|
||||
}
|
||||
|
||||
[TestCase(2)]
|
||||
[TestCase(16)]
|
||||
public void TestTeams(int count)
|
||||
{
|
||||
int[] userIds = getPlayerIds(count);
|
||||
|
||||
start(userIds, teams: true);
|
||||
loadSpectateScreen();
|
||||
|
||||
sendFrames(userIds, 1000);
|
||||
AddWaitStep("wait a bit", 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleStartRequests()
|
||||
{
|
||||
int[] userIds = getPlayerIds(2);
|
||||
|
||||
start(userIds);
|
||||
loadSpectateScreen();
|
||||
|
||||
sendFrames(userIds, 1000);
|
||||
AddWaitStep("wait a bit", 20);
|
||||
|
||||
start(userIds);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDelayedStart()
|
||||
{
|
||||
@ -88,18 +129,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGeneral()
|
||||
{
|
||||
int[] userIds = getPlayerIds(4);
|
||||
|
||||
start(userIds);
|
||||
loadSpectateScreen();
|
||||
|
||||
sendFrames(userIds, 1000);
|
||||
AddWaitStep("wait a bit", 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSpectatorPlayerInteractiveElementsHidden()
|
||||
{
|
||||
@ -434,16 +463,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
|
||||
|
||||
private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null)
|
||||
private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null, bool teams = false)
|
||||
{
|
||||
AddStep("start play", () =>
|
||||
{
|
||||
foreach (int id in userIds)
|
||||
for (int i = 0; i < userIds.Length; i++)
|
||||
{
|
||||
int id = userIds[i];
|
||||
var user = new MultiplayerRoomUser(id)
|
||||
{
|
||||
User = new APIUser { Id = id },
|
||||
Mods = mods ?? Array.Empty<APIMod>(),
|
||||
MatchState = teams ? new TeamVersusUserState { TeamID = i % 2 } : null,
|
||||
};
|
||||
|
||||
OnlinePlayDependencies.MultiplayerClient.AddUser(user, true);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user