1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 07:42:57 +08:00

Merge branch 'master' into hold-note-fade

This commit is contained in:
Dan Balasescu 2020-11-17 13:49:28 +09:00 committed by GitHub
commit cb3280af7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1190 additions and 626 deletions

View File

@ -13,6 +13,24 @@
"commands": [
"dotnet-format"
]
},
"jetbrains.resharper.globaltools": {
"version": "2020.2.4",
"commands": [
"jb"
]
},
"nvika": {
"version": "2.0.0",
"commands": [
"nvika"
]
},
"codefilesanity": {
"version": "15.0.0",
"commands": [
"CodeFileSanity"
]
}
}
}

17
.vscode/tasks.json vendored
View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Desktop",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Desktop",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -40,7 +38,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tests",
"-p:GenerateFullPaths=true",
"-m",
@ -55,7 +52,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tests",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -71,7 +67,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tournament.Tests",
"-p:GenerateFullPaths=true",
"-m",
@ -86,7 +81,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tournament.Tests",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -102,7 +96,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Benchmarks",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -111,16 +104,6 @@
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Restore (netcoreapp3.1)",
"type": "shell",
"command": "dotnet",
"args": [
"restore",
"build/Desktop.proj"
],
"problemMatcher": []
}
]
}

View File

@ -75,7 +75,6 @@ git pull
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations.
- Visual Studio Code users must run the `Restore` task before any build attempt.
You can also build and run *osu!* from the command-line with a single command:

View File

@ -1,7 +1,4 @@
#addin "nuget:?package=CodeFileSanity&version=0.0.36"
#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2020.1.3"
#tool "nuget:?package=NVika.MSBuild&version=1.0.1"
var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
///////////////////////////////////////////////////////////////////////////////
// ARGUMENTS
@ -18,23 +15,15 @@ var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
// TASKS
///////////////////////////////////////////////////////////////////////////////
// windows only because both inspectcode and nvika depend on net45
Task("InspectCode")
.WithCriteria(IsRunningOnWindows())
.Does(() => {
InspectCode(desktopSlnf, new InspectCodeSettings {
CachesHome = "inspectcode",
OutputFile = "inspectcodereport.xml",
ArgumentCustomization = arg => {
if (AppVeyor.IsRunningOnAppVeyor) // Don't flood CI output
arg.Append("--verbosity:WARN");
return arg;
},
});
var inspectcodereport = "inspectcodereport.xml";
var cacheDir = "inspectcode";
var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output
int returnCode = StartProcess(nVikaToolPath, $@"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors");
if (returnCode != 0)
throw new Exception($"inspectcode failed with return code {returnCode}");
DotNetCoreTool(rootDirectory.FullPath,
"jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}");
DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors");
});
Task("CodeFileSanity")

View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
}
]
}

View File

@ -12,17 +12,32 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestFixture]
public class CatchLegacyModConversionTest : LegacyModConversionTest
{
[TestCase(LegacyMods.Easy, new[] { typeof(CatchModEasy) })]
[TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) })]
[TestCase(LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })]
private static readonly object[][] catch_mod_mapping =
{
new object[] { LegacyMods.NoFail, new[] { typeof(CatchModNoFail) } },
new object[] { LegacyMods.Easy, new[] { typeof(CatchModEasy) } },
new object[] { LegacyMods.Hidden, new[] { typeof(CatchModHidden) } },
new object[] { LegacyMods.HardRock, new[] { typeof(CatchModHardRock) } },
new object[] { LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) } },
new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
new object[] { LegacyMods.Perfect, new[] { typeof(CatchModPerfect) } },
new object[] { LegacyMods.Cinema, new[] { typeof(CatchModCinema) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
};
[TestCaseSource(nameof(catch_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
[TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModFlashlight), typeof(CatchModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })]
[TestCase(LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime), typeof(CatchModPerfect) })]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(catch_mod_mapping))]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new CatchRuleset();
}

View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
}
]
}

View File

@ -12,19 +12,44 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture]
public class ManiaLegacyModConversionTest : LegacyModConversionTest
{
[TestCase(LegacyMods.Easy, new[] { typeof(ManiaModEasy) })]
[TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) })]
[TestCase(LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })]
private static readonly object[][] mania_mod_mapping =
{
new object[] { LegacyMods.NoFail, new[] { typeof(ManiaModNoFail) } },
new object[] { LegacyMods.Easy, new[] { typeof(ManiaModEasy) } },
new object[] { LegacyMods.Hidden, new[] { typeof(ManiaModHidden) } },
new object[] { LegacyMods.HardRock, new[] { typeof(ManiaModHardRock) } },
new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } },
new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } },
new object[] { LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) } },
new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } },
new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } },
new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } },
new object[] { LegacyMods.Key7, new[] { typeof(ManiaModKey7) } },
new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } },
new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } },
new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } },
new object[] { LegacyMods.Cinema, new[] { typeof(ManiaModCinema) } },
new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } },
new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } },
new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } },
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) } }
};
[TestCaseSource(nameof(mania_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
[TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModFlashlight), typeof(ManiaModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })]
[TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(mania_mod_mapping))]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
}

View File

@ -119,6 +119,9 @@ namespace osu.Game.Rulesets.Mania
if (mods.HasFlag(LegacyMods.Key9))
yield return new ManiaModKey9();
if (mods.HasFlag(LegacyMods.KeyCoop))
yield return new ManiaModDualStages();
if (mods.HasFlag(LegacyMods.NoFail))
yield return new ManiaModNoFail();
@ -173,13 +176,22 @@ namespace osu.Game.Rulesets.Mania
value |= LegacyMods.Key9;
break;
case ManiaModDualStages _:
value |= LegacyMods.KeyCoop;
break;
case ManiaModFadeIn _:
value |= LegacyMods.FadeIn;
value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that.
break;
case ManiaModMirror _:
value |= LegacyMods.Mirror;
break;
case ManiaModRandom _:
value |= LegacyMods.Random;
break;
}
}

View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
}
]
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Mod = new OsuModSpunOut(),
Autoplay = false,
Beatmap = singleSpinnerBeatmap,
PassCondition = () => Player.ChildrenOfType<DrawableSpinner>().Single().Progress >= 1
PassCondition = () => Player.ChildrenOfType<DrawableSpinner>().SingleOrDefault()?.Progress >= 1
});
[TestCase(null)]
@ -45,7 +45,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Mods = mods,
Autoplay = false,
Beatmap = singleSpinnerBeatmap,
PassCondition = () => Precision.AlmostEquals(Player.ChildrenOfType<SpinnerSpmCounter>().Single().SpinsPerMinute, 286, 1)
PassCondition = () =>
{
var counter = Player.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
}
});
}

View File

@ -12,18 +12,36 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture]
public class OsuLegacyModConversionTest : LegacyModConversionTest
{
[TestCase(LegacyMods.Easy, new[] { typeof(OsuModEasy) })]
[TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) })]
[TestCase(LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })]
private static readonly object[][] osu_mod_mapping =
{
new object[] { LegacyMods.NoFail, new[] { typeof(OsuModNoFail) } },
new object[] { LegacyMods.Easy, new[] { typeof(OsuModEasy) } },
new object[] { LegacyMods.TouchDevice, new[] { typeof(OsuModTouchDevice) } },
new object[] { LegacyMods.Hidden, new[] { typeof(OsuModHidden) } },
new object[] { LegacyMods.HardRock, new[] { typeof(OsuModHardRock) } },
new object[] { LegacyMods.SuddenDeath, new[] { typeof(OsuModSuddenDeath) } },
new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } },
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
new object[] { LegacyMods.Perfect, new[] { typeof(OsuModPerfect) } },
new object[] { LegacyMods.Cinema, new[] { typeof(OsuModCinema) } },
new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
};
[TestCaseSource(nameof(osu_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
[TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModFlashlight), typeof(OsuModFlashlight) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(OsuModPerfect) })]
[TestCase(LegacyMods.SuddenDeath, new[] { typeof(OsuModSuddenDeath) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime), typeof(OsuModPerfect) })]
[TestCase(LegacyMods.SpunOut | LegacyMods.Easy, new[] { typeof(OsuModSpunOut), typeof(OsuModEasy) })]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(osu_mod_mapping))]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new OsuRuleset();
}

View File

@ -1,20 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSliderApplication : OsuTestScene
{
[Resolved]
private SkinManager skinManager { get; set; }
[Test]
public void TestApplyNewSlider()
{
@ -50,6 +60,41 @@ namespace osu.Game.Rulesets.Osu.Tests
}), null));
}
[Test]
public void TestBallTintChangedOnAccentChange()
{
DrawableSlider dho = null;
AddStep("create slider", () =>
{
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
Child = new SkinProvidingContainer(tintingSkin)
{
RelativeSizeAxes = Axes.Both,
Child = dho = new DrawableSlider(prepareObject(new Slider
{
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0,
StartTime = Time.Current,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(150, 100),
new Vector2(300, 0),
})
}))
};
});
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
AddAssert("ball is white", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
AddAssert("ball is red", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.Red);
}
private Slider prepareObject(Slider slider)
{
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
}
private DrawableSlider slider;
private DrawableSlider drawableSlider;
[SetUpSteps]
public override void SetUpSteps()
@ -68,7 +68,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex);
addSeekStep(startTime);
retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
setSnaking(true);
ensureSnakingIn(startTime + fade_in_modifier);
@ -93,7 +94,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex);
addSeekStep(startTime);
retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
setSnaking(false);
ensureNoSnakingIn(startTime + fade_in_modifier);
@ -127,9 +129,8 @@ namespace osu.Game.Rulesets.Osu.Tests
checkPositionChange(16600, sliderRepeat, positionDecreased);
}
private void retrieveDrawableSlider(int index) =>
AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () =>
slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index));
private void retrieveDrawableSlider(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased);
private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame);
@ -150,13 +151,13 @@ namespace osu.Game.Rulesets.Osu.Tests
private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)sliderStart : sliderEnd;
private List<Vector2> sliderCurve => ((PlaySliderBody)slider.Body.Drawable).CurrentCurve;
private List<Vector2> sliderCurve => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private Vector2 sliderStart() => sliderCurve.First();
private Vector2 sliderEnd() => sliderCurve.Last();
private Vector2 sliderRepeat()
{
var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(1);
var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]);
var repeat = drawable.ChildrenOfType<Container<DrawableSliderRepeat>>().First().Children.First();
return repeat.Position;
}

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public class TestSceneSpinnerApplication : OsuTestScene
{
[Test]
public void TestApplyNewCircle()
public void TestApplyNewSpinner()
{
DrawableSpinner dho = null;
@ -23,18 +23,23 @@ namespace osu.Game.Rulesets.Osu.Tests
{
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0,
Duration = 0,
Duration = 500,
}))
{
Clock = new FramedClock(new StopwatchClock())
});
AddStep("rotate some", () => dho.RotationTracker.AddRotation(180));
AddAssert("rotation is set", () => dho.Result.RateAdjustedRotation == 180);
AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner
{
Position = new Vector2(256, 192),
ComboIndex = 1,
Duration = 1000,
}), null));
AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0);
}
private Spinner prepareObject(Spinner circle)

View File

@ -62,11 +62,11 @@ namespace osu.Game.Rulesets.Osu.Tests
trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
});
AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100));
addSeekStep(0);
AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance));
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100));
AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100));
}
[Test]
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests
finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.RateAdjustedRotation);
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
addSeekStep(2500);
AddAssert("disc rotation rewound",
@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation rewound",
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000);
AddAssert("is disc rotation almost same",
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("is symbol rotation almost same",
() => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation, 100));
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation, 100));
}
[Test]
@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult;
});
addSeekStep(0);

View File

@ -0,0 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditPool<T> : DrawableOsuPool<T>
where T : DrawableHitObject, new()
{
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
private const double editor_hit_object_fade_out_extension = 700;
public DrawableOsuEditPool(Func<DrawableHitObject, double, bool> checkHittable, Action<Drawable> onLoaded, int initialSize, int? maximumSize = null)
: base(checkHittable, onLoaded, initialSize, maximumSize)
{
}
protected override T CreateNewDrawable() => base.CreateNewDrawable().With(d => d.ApplyCustomUpdateState += updateState);
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle)
return;
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
switch (hitObject)
{
default:
// there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.)
return;
case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
case DrawableHitCircle circle: // also handles slider heads
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break;
}
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
}
}
}

View File

@ -2,13 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
@ -17,62 +13,21 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditRuleset : DrawableOsuRuleset
{
/// <summary>
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
/// </summary>
private const double editor_hit_object_fade_out_extension = 700;
public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(ruleset, beatmap, mods)
{
}
public override DrawableHitObject<OsuHitObject> CreateDrawableRepresentation(OsuHitObject h)
=> base.CreateDrawableRepresentation(h)?.With(d => d.ApplyCustomUpdateState += updateState);
private void updateState(DrawableHitObject hitObject, ArmedState state)
{
if (state == ArmedState.Idle)
return;
// adjust the visuals of certain object types to make them stay on screen for longer than usual.
switch (hitObject)
{
default:
// there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.)
return;
case DrawableSlider _:
// no specifics to sliders but let them fade slower below.
break;
case DrawableHitCircle circle: // also handles slider heads
circle.ApproachCircle
.FadeOutFromOne(editor_hit_object_fade_out_extension)
.Expire();
break;
}
// Get the existing fade out transform
var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha));
if (existing == null)
return;
hitObject.RemoveTransform(existing);
using (hitObject.BeginAbsoluteSequence(existing.StartTime))
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
}
protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor();
protected override Playfield CreatePlayfield() => new OsuEditPlayfield();
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { Size = Vector2.One };
private class OsuPlayfieldNoCursor : OsuPlayfield
private class OsuEditPlayfield : OsuPlayfield
{
protected override GameplayCursorContainer CreateCursor() => null;
protected override DrawablePool<TDrawable> CreatePool<TDrawable>(int initialSize, int? maximumSize = null)
=> new DrawableOsuEditPool<TDrawable>(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize);
}
}
}

View File

@ -207,11 +207,17 @@ namespace osu.Game.Rulesets.Osu.Edit
Quad quad = getSurroundingQuad(hitObjects);
if (quad.TopLeft.X + delta.X < 0 ||
quad.TopLeft.Y + delta.Y < 0 ||
quad.BottomRight.X + delta.X > DrawWidth ||
quad.BottomRight.Y + delta.Y > DrawHeight)
return false;
Vector2 newTopLeft = quad.TopLeft + delta;
if (newTopLeft.X < 0)
delta.X -= newTopLeft.X;
if (newTopLeft.Y < 0)
delta.Y -= newTopLeft.Y;
Vector2 newBottomRight = quad.BottomRight + delta;
if (newBottomRight.X > DrawWidth)
delta.X -= newBottomRight.X - DrawWidth;
if (newBottomRight.Y > DrawHeight)
delta.Y -= newBottomRight.Y - DrawHeight;
foreach (var h in hitObjects)
h.Position += delta;

View 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 osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Judgements
{
public class OsuSpinnerJudgementResult : OsuJudgementResult
{
/// <summary>
/// The <see cref="Spinner"/>.
/// </summary>
public Spinner Spinner => (Spinner)HitObject;
/// <summary>
/// The total rotation performed on the spinner disc, disregarding the spin direction,
/// adjusted for the track's playback rate.
/// </summary>
/// <remarks>
/// <para>
/// This value is always non-negative and is monotonically increasing with time
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
/// </para>
/// <para>
/// The rotation from each frame is multiplied by the clock's current playback rate.
/// The reason this is done is to ensure that spinners give the same score and require the same number of spins
/// regardless of whether speed-modifying mods are applied.
/// </para>
/// </remarks>
/// <example>
/// Assuming no speed-modifying mods are active,
/// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
/// this property will return the value of 720 (as opposed to 0).
/// If Double Time is active instead (with a speed multiplier of 1.5x),
/// in the same scenario the property will return 720 * 1.5 = 1080.
/// </example>
public float RateAdjustedRotation;
/// <summary>
/// Time instant at which the spinner has been completed (the user has executed all required spins).
/// Will be null if all required spins haven't been completed.
/// </summary>
public double? TimeCompleted;
public OsuSpinnerJudgementResult(HitObject hitObject, Judgement judgement)
: base(hitObject, judgement)
{
}
}
}

View File

@ -28,10 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var d in drawables)
{
d.HitObjectApplied += applyFadeInAdjustment;
applyFadeInAdjustment(d);
}
base.ApplyToDrawableHitObjects(drawables);
}

View File

@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Container scaleContainer;
private InputManager inputManager;
public DrawableHitCircle()
: this(null)
{
}
public DrawableHitCircle([CanBeNull] HitCircle h = null)
: base(h)
{

View File

@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private OsuInputManager osuActionInputManager;
internal OsuInputManager OsuActionInputManager => osuActionInputManager ??= GetContainingInputManager() as OsuInputManager;
protected virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
protected override void UpdateInitialTransforms()
{

View File

@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableOsuPool<T> : DrawablePool<T>
where T : DrawableHitObject, new()
{
private readonly Func<DrawableHitObject, double, bool> checkHittable;
private readonly Action<Drawable> onLoaded;
public DrawableOsuPool(Func<DrawableHitObject, double, bool> checkHittable, Action<Drawable> onLoaded, int initialSize, int? maximumSize = null)
: base(initialSize, maximumSize)
{
this.checkHittable = checkHittable;
this.onLoaded = onLoaded;
}
protected override T CreateNewDrawable() => base.CreateNewDrawable().With(o =>
{
var osuObject = (DrawableOsuHitObject)(object)o;
osuObject.CheckHittable = checkHittable;
osuObject.OnLoadComplete += onLoaded;
});
}
}

View File

@ -40,6 +40,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Container<DrawableSliderTail> tailContainer;
private Container<DrawableSliderTick> tickContainer;
private Container<DrawableSliderRepeat> repeatContainer;
private Container<PausableSkinnableSound> samplesContainer;
public DrawableSlider()
: this(null)
{
}
public DrawableSlider([CanBeNull] Slider s = null)
: base(s)
@ -63,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Alpha = 0
},
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
samplesContainer = new Container<PausableSkinnableSound> { RelativeSizeAxes = Axes.Both }
};
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
@ -73,6 +80,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
updateBallTint();
}, true);
Tracking.BindValueChanged(updateSlidingSample);
@ -100,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.LoadSamples();
slidingSample?.Expire();
samplesContainer.Clear();
slidingSample = null;
var firstSample = HitObject.Samples.FirstOrDefault();
@ -110,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "sliderslide";
AddInternal(slidingSample = new PausableSkinnableSound(clone)
samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone)
{
Looping = true
});
@ -159,10 +167,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.ClearNestedHitObjects();
headContainer.Clear();
tailContainer.Clear();
repeatContainer.Clear();
tickContainer.Clear();
headContainer.Clear(false);
tailContainer.Clear(false);
repeatContainer.Clear(false);
tickContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
@ -173,17 +181,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return new DrawableSliderTail(tail);
case SliderHeadCircle head:
return new DrawableSliderHead(HitObject, head)
{
OnShake = Shake,
CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true
};
return new DrawableSliderHead(head);
case SliderTick tick:
return new DrawableSliderTick(tick) { Position = tick.Position - HitObject.Position };
return new DrawableSliderTick(tick);
case SliderRepeat repeat:
return new DrawableSliderRepeat(repeat, this) { Position = repeat.Position - HitObject.Position };
return new DrawableSliderRepeat(repeat);
}
return base.CreateNestedHitObject(hitObject);
@ -241,7 +245,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.ApplySkin(skin, allowFallback);
bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
updateBallTint();
}
private void updateBallTint()
{
if (CurrentSkin == null)
return;
bool allowBallTint = CurrentSkin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
}

View File

@ -4,6 +4,8 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -14,21 +16,43 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
private readonly Slider slider;
private DrawableSlider drawableSlider;
public DrawableSliderHead(Slider slider, SliderHeadCircle h)
private Slider slider => drawableSlider?.HitObject;
public DrawableSliderHead()
{
}
public DrawableSliderHead(SliderHeadCircle h)
: base(h)
{
this.slider = slider;
}
[BackgroundDependencyLoader]
private void load()
{
pathVersion.BindTo(slider.Path.Version);
PositionBindable.BindValueChanged(_ => updatePosition());
pathVersion.BindValueChanged(_ => updatePosition(), true);
pathVersion.BindValueChanged(_ => updatePosition());
}
protected override void OnFree(HitObject hitObject)
{
base.OnFree(hitObject);
pathVersion.UnbindFrom(drawableSlider.PathVersion);
}
protected override void OnParentReceived(DrawableHitObject parent)
{
base.OnParentReceived(parent);
drawableSlider = (DrawableSlider)parent;
pathVersion.BindTo(drawableSlider.PathVersion);
OnShake = drawableSlider.Shake;
CheckHittable = (d, t) => drawableSlider.CheckHittable?.Invoke(d, t) ?? true;
}
protected override void Update()
@ -44,8 +68,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Action<double> OnShake;
protected override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
private void updatePosition() => Position = HitObject.Position - slider.Position;
private void updatePosition()
{
if (slider != null)
Position = HitObject.Position - slider.Position;
}
}
}

View File

@ -16,8 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
{
private readonly SliderRepeat sliderRepeat;
private readonly DrawableSlider drawableSlider;
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
private double animDuration;
@ -27,11 +26,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => false;
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
private DrawableSlider drawableSlider;
public DrawableSliderRepeat()
: base(null)
{
}
public DrawableSliderRepeat(SliderRepeat sliderRepeat)
: base(sliderRepeat)
{
this.sliderRepeat = sliderRepeat;
this.drawableSlider = drawableSlider;
}
[BackgroundDependencyLoader]
@ -53,18 +57,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
};
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
protected override void OnParentReceived(DrawableHitObject parent)
{
base.OnParentReceived(parent);
drawableSlider = (DrawableSlider)parent;
Position = HitObject.Position - drawableSlider.Position;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (sliderRepeat.StartTime <= Time.Current)
if (HitObject.StartTime <= Time.Current)
ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
protected override void UpdateInitialTransforms()
{
animDuration = Math.Min(300, sliderRepeat.SpanDuration);
animDuration = Math.Min(300, HitObject.SpanDuration);
this.Animate(
d => d.FadeIn(animDuration),
@ -100,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// When the repeat is hit, the arrow should fade out on spot rather than following the slider
if (IsHit) return;
bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0;
bool isRepeatAtEnd = HitObject.RepeatIndex % 2 == 0;
List<Vector2> curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
Position = isRepeatAtEnd ? end : start;

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
{
private readonly SliderTailCircle tailCircle;
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
/// <summary>
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
@ -25,10 +25,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private SkinnableDrawable circlePiece;
private Container scaleContainer;
public DrawableSliderTail()
: base(null)
{
}
public DrawableSliderTail(SliderTailCircle tailCircle)
: base(tailCircle)
{
this.tailCircle = tailCircle;
}
[BackgroundDependencyLoader]
@ -52,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
},
};
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
protected override void UpdateInitialTransforms()
@ -92,6 +96,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
public void UpdateSnakingPosition(Vector2 start, Vector2 end) =>
Position = tailCircle.RepeatIndex % 2 == 0 ? end : start;
Position = HitObject.RepeatIndex % 2 == 0 ? end : start;
}
}

View File

@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private SkinnableDrawable scaleContainer;
public DrawableSliderTick()
: base(null)
{
}
public DrawableSliderTick(SliderTick sliderTick)
: base(sliderTick)
{
@ -54,7 +59,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre,
};
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
protected override void OnParentReceived(DrawableHitObject parent)
{
base.OnParentReceived(parent);
Position = HitObject.Position - ((DrawableSlider)parent).HitObject.Position;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)

View File

@ -10,8 +10,10 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Scoring;
@ -24,15 +26,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public new Spinner HitObject => (Spinner)base.HitObject;
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
public SpinnerRotationTracker RotationTracker { get; private set; }
public SpinnerSpmCounter SpmCounter { get; private set; }
private Container<DrawableSpinnerTick> ticks;
private SpinnerBonusDisplay bonusDisplay;
private Container<PausableSkinnableSound> samplesContainer;
private Bindable<bool> isSpinning;
private bool spinnerFrequencyModulate;
public DrawableSpinner()
: this(null)
{
}
public DrawableSpinner([CanBeNull] Spinner s = null)
: base(s)
{
@ -70,7 +80,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = -120,
}
},
samplesContainer = new Container<PausableSkinnableSound> { RelativeSizeAxes = Axes.Both }
};
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
@ -92,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.LoadSamples();
spinningSample?.Expire();
samplesContainer.Clear();
spinningSample = null;
var firstSample = HitObject.Samples.FirstOrDefault();
@ -102,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "spinnerspin";
AddInternal(spinningSample = new PausableSkinnableSound(clone)
samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone)
{
Volume = { Value = 0 },
Looping = true,
@ -155,7 +166,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
ticks.Clear();
ticks.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
@ -190,15 +201,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// these become implicitly hit.
return 1;
return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1);
return Math.Clamp(Result.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1);
}
}
protected override JudgementResult CreateResult(Judgement judgement) => new OsuSpinnerJudgementResult(HitObject, judgement);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (Time.Current < HitObject.StartTime) return;
RotationTracker.Complete.Value = Progress >= 1;
if (Progress >= 1)
Result.TimeCompleted ??= Time.Current;
if (userTriggered || Time.Current < HitObject.EndTime)
return;
@ -237,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
SpmCounter.SetRotation(RotationTracker.RateAdjustedRotation);
SpmCounter.SetRotation(Result.RateAdjustedRotation);
updateBonusScore();
}
@ -249,7 +263,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (ticks.Count == 0)
return;
int spins = (int)(RotationTracker.RateAdjustedRotation / 360);
int spins = (int)(Result.RateAdjustedRotation / 360);
if (spins < wholeSpins)
{

View File

@ -5,6 +5,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSpinnerBonusTick : DrawableSpinnerTick
{
public DrawableSpinnerBonusTick()
: base(null)
{
}
public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick)
: base(spinnerTick)
{

View File

@ -7,6 +7,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public override bool DisplayResult => false;
public DrawableSpinnerTick()
: base(null)
{
}
public DrawableSpinnerTick(SpinnerTick spinnerTick)
: base(spinnerTick)
{

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -28,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private SpinnerTicks ticks;
private int wholeRotationCount;
private readonly BindableBool complete = new BindableBool();
private SpinnerFill fill;
private Container mainContainer;
@ -89,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
base.LoadComplete();
drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
@ -99,7 +101,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
base.Update();
if (drawableSpinner.RotationTracker.Complete.Value)
complete.Value = Time.Current >= drawableSpinner.Result.TimeCompleted;
if (complete.Value)
{
if (checkNewRotationCount)
{
@ -194,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
get
{
int rotations = (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360);
int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360);
if (wholeRotationCount == rotations) return false;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Play;
using osuTK;
@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public SpinnerRotationTracker(DrawableSpinner drawableSpinner)
{
this.drawableSpinner = drawableSpinner;
drawableSpinner.HitObjectApplied += resetState;
RelativeSizeAxes = Axes.Both;
}
@ -30,32 +32,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public bool Tracking { get; set; }
public readonly BindableBool Complete = new BindableBool();
/// <summary>
/// The total rotation performed on the spinner disc, disregarding the spin direction,
/// adjusted for the track's playback rate.
/// </summary>
/// <remarks>
/// <para>
/// This value is always non-negative and is monotonically increasing with time
/// (i.e. will only increase if time is passing forward, but can decrease during rewind).
/// </para>
/// <para>
/// The rotation from each frame is multiplied by the clock's current playback rate.
/// The reason this is done is to ensure that spinners give the same score and require the same number of spins
/// regardless of whether speed-modifying mods are applied.
/// </para>
/// </remarks>
/// <example>
/// Assuming no speed-modifying mods are active,
/// if the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
/// this property will return the value of 720 (as opposed to 0 for <see cref="Drawable.Rotation"/>).
/// If Double Time is active instead (with a speed multiplier of 1.5x),
/// in the same scenario the property will return 720 * 1.5 = 1080.
/// </example>
public float RateAdjustedRotation { get; private set; }
/// <summary>
/// Whether the spinning is spinning at a reasonable speed to be considered visually spinning.
/// </summary>
@ -131,7 +107,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
currentRotation += angle;
// rate has to be applied each frame, because it's not guaranteed to be constant throughout playback
// (see: ModTimeRamp)
RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate));
drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate));
}
private void resetState(DrawableHitObject obj)
{
Tracking = false;
IsSpinning.Value = false;
mousePosition = default;
lastAngle = currentRotation = Rotation = 0;
rotationTransferred = false;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner != null)
drawableSpinner.HitObjectApplied -= resetState;
}
}
}

View File

@ -107,6 +107,35 @@ namespace osu.Game.Rulesets.Osu
yield return new OsuModTouchDevice();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
{
var value = base.ConvertToLegacyMods(mods);
foreach (var mod in mods)
{
switch (mod)
{
case OsuModAutopilot _:
value |= LegacyMods.Autopilot;
break;
case OsuModSpunOut _:
value |= LegacyMods.SpunOut;
break;
case OsuModTarget _:
value |= LegacyMods.Target;
break;
case OsuModTouchDevice _:
value |= LegacyMods.TouchDevice;
break;
}
}
return value;
}
public override IEnumerable<Mod> GetModsFor(ModType type)
{
switch (type)

View File

@ -60,7 +60,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.LoadComplete();
completed.BindTo(DrawableSpinner.RotationTracker.Complete);
completed.BindValueChanged(onCompletedChanged, true);
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
@ -93,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
}
protected override void Update()
{
base.Update();
completed.Value = Time.Current >= DrawableSpinner.Result.TimeCompleted;
}
protected virtual void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
switch (drawableHitObject)

View File

@ -12,7 +12,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@ -24,11 +23,15 @@ namespace osu.Game.Rulesets.Osu.UI
{
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
: base(ruleset, beatmap, mods)
{
}
public override DrawableHitObject<OsuHitObject> CreateDrawableRepresentation(OsuHitObject h) => null;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor
protected override Playfield CreatePlayfield() => new OsuPlayfield();
@ -39,23 +42,6 @@ namespace osu.Game.Rulesets.Osu.UI
protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay();
public override DrawableHitObject<OsuHitObject> CreateDrawableRepresentation(OsuHitObject h)
{
switch (h)
{
case HitCircle circle:
return new DrawableHitCircle(circle);
case Slider slider:
return new DrawableSlider(slider);
case Spinner spinner:
return new DrawableSpinner(spinner);
}
return null;
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new OsuReplayRecorder(replay);

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.Osu.Scoring;
@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.UI
{
public class OsuPlayfield : Playfield
{
public readonly Func<DrawableHitObject, double, bool> CheckHittable;
private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies;
@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.UI
};
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
CheckHittable = hitPolicy.IsHittable;
var hitWindows = new OsuHitWindows();
@ -85,45 +89,70 @@ namespace osu.Game.Rulesets.Osu.UI
poolDictionary.Add(result, new DrawableJudgementPool(result));
AddRangeInternal(poolDictionary.Values);
NewResult += onNewResult;
}
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
{
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
registerPool<HitCircle, DrawableHitCircle>(10, 100);
registerPool<Slider, DrawableSlider>(10, 100);
registerPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
registerPool<SliderTailCircle, DrawableSliderTail>(10, 100);
registerPool<SliderTick, DrawableSliderTick>(10, 100);
registerPool<SliderRepeat, DrawableSliderRepeat>(5, 50);
registerPool<Spinner, DrawableSpinner>(2, 20);
registerPool<SpinnerTick, DrawableSpinnerTick>(10, 100);
registerPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100);
}
public override void Add(DrawableHitObject h)
{
DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h;
private void registerPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
=> RegisterPool<TObject, TDrawable>(CreatePool<TDrawable>(initialSize, maximumSize));
h.OnNewResult += onNewResult;
h.OnLoadComplete += d =>
protected virtual DrawablePool<TDrawable> CreatePool<TDrawable>(int initialSize, int? maximumSize = null)
where TDrawable : DrawableHitObject, new()
=> new DrawableOsuPool<TDrawable>(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize);
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
protected override void OnHitObjectAdded(HitObject hitObject)
{
base.OnHitObjectAdded(hitObject);
followPoints.AddFollowPoints((OsuHitObject)hitObject);
}
protected override void OnHitObjectRemoved(HitObject hitObject)
{
base.OnHitObjectRemoved(hitObject);
followPoints.RemoveFollowPoints((OsuHitObject)hitObject);
}
public void OnHitObjectLoaded(Drawable drawable)
{
switch (drawable)
{
if (d is DrawableSpinner)
spinnerProxies.Add(d.CreateProxy());
case DrawableSliderHead _:
case DrawableSliderTail _:
case DrawableSliderTick _:
case DrawableSliderRepeat _:
case DrawableSpinnerTick _:
break;
if (d is IDrawableHitObjectWithProxiedApproach c)
approachCircles.Add(c.ProxiedLayer.CreateProxy());
};
case DrawableSpinner _:
spinnerProxies.Add(drawable.CreateProxy());
break;
base.Add(h);
osuHitObject.CheckHittable = hitPolicy.IsHittable;
followPoints.AddFollowPoints(osuHitObject.HitObject);
}
public override bool Remove(DrawableHitObject h)
{
DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h;
bool result = base.Remove(h);
if (result)
followPoints.RemoveFollowPoints(osuHitObject.HitObject);
return result;
case IDrawableHitObjectWithProxiedApproach approach:
approachCircles.Add(approach.ProxiedLayer.CreateProxy());
break;
}
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
@ -166,5 +195,15 @@ namespace osu.Game.Rulesets.Osu.UI
return judgement;
}
}
private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry
{
public OsuHitObjectLifetimeEntry(HitObject hitObject)
: base(hitObject)
{
}
protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt;
}
}
}

View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
}
]
}

View File

@ -12,17 +12,33 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestFixture]
public class TaikoLegacyModConversionTest : LegacyModConversionTest
{
[TestCase(LegacyMods.Easy, new[] { typeof(TaikoModEasy) })]
[TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) })]
[TestCase(LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) })]
[TestCase(LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) })]
private static readonly object[][] taiko_mod_mapping =
{
new object[] { LegacyMods.NoFail, new[] { typeof(TaikoModNoFail) } },
new object[] { LegacyMods.Easy, new[] { typeof(TaikoModEasy) } },
new object[] { LegacyMods.Hidden, new[] { typeof(TaikoModHidden) } },
new object[] { LegacyMods.HardRock, new[] { typeof(TaikoModHardRock) } },
new object[] { LegacyMods.SuddenDeath, new[] { typeof(TaikoModSuddenDeath) } },
new object[] { LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) } },
new object[] { LegacyMods.Relax, new[] { typeof(TaikoModRelax) } },
new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } },
new object[] { LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) } },
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
new object[] { LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) } },
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
new object[] { LegacyMods.Cinema, new[] { typeof(TaikoModCinema) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }
};
[TestCaseSource(nameof(taiko_mod_mapping))]
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })]
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })]
[TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModFlashlight), typeof(TaikoModNightcore) })]
[TestCase(LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) })]
[TestCase(LegacyMods.SuddenDeath, new[] { typeof(TaikoModSuddenDeath) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime), typeof(TaikoModPerfect) })]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
[TestCaseSource(nameof(taiko_mod_mapping))]
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
protected override Ruleset CreateRuleset() => new TaikoRuleset();
}

View File

@ -92,6 +92,19 @@ namespace osu.Game.Rulesets.Taiko
if (mods.HasFlag(LegacyMods.Relax))
yield return new TaikoModRelax();
if (mods.HasFlag(LegacyMods.Random))
yield return new TaikoModRandom();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
{
var value = base.ConvertToLegacyMods(mods);
if (mods.OfType<TaikoModRandom>().Any())
value |= LegacyMods.Random;
return value;
}
public override IEnumerable<Mod> GetModsFor(ModType type)

View File

@ -3,7 +3,6 @@
using NUnit.Framework;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Beatmaps;
@ -12,7 +11,6 @@ using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Tests.Gameplay
{
[HeadlessTest]
public class TestSceneHitObjectSamples : HitObjectSampleTest
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();

View File

@ -167,6 +167,21 @@ namespace osu.Game.Tests.Visual.Components
AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0);
}
[Test]
public void TestOwnerNotRegistered()
{
PreviewTrack track = null;
AddStep("get track", () => Add(new TestTrackOwner(track = getTrack(), registerAsOwner: false)));
AddUntilStep("wait for loaded", () => track.IsLoaded);
AddStep("start track", () => track.Start());
AddUntilStep("track is running", () => track.IsRunning);
AddStep("cancel from anyone", () => trackManager.StopAnyPlaying(this));
AddAssert("track stopped", () => !track.IsRunning);
}
private TestPreviewTrack getTrack() => (TestPreviewTrack)trackManager.Get(null);
private TestPreviewTrack getOwnedTrack()
@ -181,10 +196,12 @@ namespace osu.Game.Tests.Visual.Components
private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner
{
private readonly PreviewTrack track;
private readonly bool registerAsOwner;
public TestTrackOwner(PreviewTrack track)
public TestTrackOwner(PreviewTrack track, bool registerAsOwner = true)
{
this.track = track;
this.registerAsOwner = registerAsOwner;
}
[BackgroundDependencyLoader]
@ -196,7 +213,8 @@ namespace osu.Game.Tests.Visual.Components
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<IPreviewTrackOwner>(this);
if (registerAsOwner)
dependencies.CacheAs<IPreviewTrackOwner>(this);
return dependencies;
}
}

View File

@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
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.Tests.Beatmaps;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorQuickDelete : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First();
[Test]
public void TestQuickDeleteRemovesObject()
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("move mouse to object", () =>
{
var pos = blueprintContainer.ChildrenOfType<HitCirclePiece>().First().ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
}
[Test]
public void TestQuickDeleteRemovesSliderControlPoint()
{
Slider slider = new Slider { StartTime = 1000 };
PathControlPoint[] points =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(50, 0)),
new PathControlPoint(new Vector2(100, 0))
};
AddStep("add slider", () =>
{
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
});
AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to controlpoint", () =>
{
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2);
// second click should nuke the object completely.
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
}
}

View File

@ -23,11 +23,13 @@ namespace osu.Game.Tests.Visual.Gameplay
DrawableSample[] samples = null;
ISamplePlaybackDisabler sampleDisabler = null;
AddStep("get variables", () =>
AddUntilStep("get variables", () =>
{
sampleDisabler = Player;
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).FirstOrDefault();
samples = slider?.ChildrenOfType<DrawableSample>().ToArray();
return slider != null;
});
AddUntilStep("wait for slider sliding then seek", () =>

View File

@ -23,17 +23,15 @@ 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.Play.PlayerSettings;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestScenePlayerLoader : OsuManualInputManagerTestScene
public class TestScenePlayerLoader : ScreenTestScene
{
private TestPlayerLoader loader;
private TestPlayerLoaderContainer container;
private TestPlayer player;
private bool epilepsyWarning;
@ -44,21 +42,46 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private SessionStatics sessionStatics { get; set; }
[Cached]
private readonly NotificationOverlay notificationOverlay;
[Cached]
private readonly VolumeOverlay volumeOverlay;
private readonly ChangelogOverlay changelogOverlay;
public TestScenePlayerLoader()
{
AddRange(new Drawable[]
{
notificationOverlay = new NotificationOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
volumeOverlay = new VolumeOverlay
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
},
changelogOverlay = new ChangelogOverlay()
});
}
[SetUp]
public void Setup() => Schedule(() =>
{
player = null;
audioManager.Volume.SetDefault();
});
/// <summary>
/// Sets the input manager child to a new test player loader container instance.
/// </summary>
/// <param name="interactive">If the test player should behave like the production one.</param>
/// <param name="beforeLoadAction">An action to run before player load but after bindable leases are returned.</param>
public void ResetPlayer(bool interactive, Action beforeLoadAction = null)
private void resetPlayer(bool interactive, Action beforeLoadAction = null)
{
player = null;
audioManager.Volume.SetDefault();
InputManager.Clear();
container = new TestPlayerLoaderContainer(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
@ -67,13 +90,13 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track);
InputManager.Child = container;
LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
}
[Test]
public void TestEarlyExitBeforePlayerConstruction()
{
AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("exit loader", () => loader.Exit());
AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
@ -90,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestEarlyExitAfterPlayerConstruction()
{
AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1);
AddUntilStep("wait for non-null player", () => player != null);
@ -104,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestBlockLoadViaMouseMovement()
{
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("wait for load ready", () =>
@ -129,20 +152,18 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestBlockLoadViaFocus()
{
OsuFocusedOverlayContainer overlay = null;
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("show focused overlay", () => { container.Add(overlay = new ChangelogOverlay { State = { Value = Visibility.Visible } }); });
AddUntilStep("overlay visible", () => overlay.IsPresent);
AddStep("show focused overlay", () => changelogOverlay.Show());
AddUntilStep("overlay visible", () => changelogOverlay.IsPresent);
AddUntilStep("wait for load ready", () => player.LoadState == LoadState.Ready);
AddUntilStep("wait for load ready", () => player?.LoadState == LoadState.Ready);
AddRepeatStep("twiddle thumbs", () => { }, 20);
AddAssert("loader still active", () => loader.IsCurrentScreen());
AddStep("hide overlay", () => overlay.Hide());
AddStep("hide overlay", () => changelogOverlay.Hide());
AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
}
@ -151,15 +172,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{
SlowLoadPlayer slowPlayer = null;
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen());
AddStep("load slow dummy beatmap", () =>
{
InputManager.Child = container = new TestPlayerLoaderContainer(
loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000);
});
@ -173,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay
TestMod playerMod1 = null;
TestMod playerMod2 = null;
AddStep("load player", () => { ResetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); });
AddStep("load player", () => { resetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); });
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
@ -201,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var testMod = new TestMod();
AddStep("load player", () => ResetPlayer(true));
AddStep("load player", () => resetPlayer(true));
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("set test mod in loader", () => loader.Mods.Value = new[] { testMod });
@ -223,7 +238,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestMutedNotificationMuteButton()
{
addVolumeSteps("mute button", () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
addVolumeSteps("mute button", () => volumeOverlay.IsMuted.Value = true, () => !volumeOverlay.IsMuted.Value);
}
/// <remarks>
@ -236,13 +251,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).Value = false);
AddStep("load player", () => ResetPlayer(false, beforeLoad));
AddStep("load player", () => resetPlayer(false, beforeLoad));
AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1);
AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1);
AddStep("click notification", () =>
{
var scrollContainer = (OsuScrollContainer)container.NotificationOverlay.Children.Last();
var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last();
var flowContainer = scrollContainer.Children.OfType<FillFlowContainer<NotificationSection>>().First();
var notification = flowContainer.First();
@ -260,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEpilepsyWarning(bool warning)
{
AddStep("change epilepsy warning", () => epilepsyWarning = warning);
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
@ -277,7 +292,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEpilepsyWarningEarlyExit()
{
AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
@ -287,42 +302,6 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
}
private class TestPlayerLoaderContainer : Container
{
[Cached]
public readonly NotificationOverlay NotificationOverlay;
[Cached]
public readonly VolumeOverlay VolumeOverlay;
public TestPlayerLoaderContainer(IScreen screen)
{
RelativeSizeAxes = Axes.Both;
OsuScreenStack stack;
InternalChildren = new Drawable[]
{
stack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both,
},
NotificationOverlay = new NotificationOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
VolumeOverlay = new VolumeOverlay
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
}
};
stack.Push(screen);
}
}
private class TestPlayerLoader : PlayerLoader
{
public new VisualSettings VisualSettings => base.VisualSettings;

View File

@ -57,6 +57,43 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("DHO reused", () => this.ChildrenOfType<DrawableTestHitObject>().Single() == firstObject);
}
[Test]
public void TestCustomTransformsClearedBetweenReuses()
{
ManualClock clock = null;
createTest(new Beatmap
{
HitObjects =
{
new HitObject(),
new HitObject { StartTime = 2000 }
}
}, 1, () => new FramedClock(clock = new ManualClock()));
DrawableTestHitObject firstObject = null;
Vector2 position = default;
AddUntilStep("first object shown", () => this.ChildrenOfType<DrawableTestHitObject>().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[0]);
AddStep("get DHO", () => firstObject = this.ChildrenOfType<DrawableTestHitObject>().Single());
AddStep("store position", () => position = firstObject.Position);
AddStep("add custom transform", () => firstObject.ApplyCustomUpdateState += onStateUpdate);
AddStep("fast forward past first object", () => clock.CurrentTime = 1500);
AddStep("unapply custom transform", () => firstObject.ApplyCustomUpdateState -= onStateUpdate);
AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime);
AddUntilStep("second object shown", () => this.ChildrenOfType<DrawableTestHitObject>().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[1]);
AddAssert("DHO reused", () => this.ChildrenOfType<DrawableTestHitObject>().Single() == firstObject);
AddAssert("object in new position", () => firstObject.Position != position);
void onStateUpdate(DrawableHitObject hitObject, ArmedState state)
{
using (hitObject.BeginAbsoluteSequence(hitObject.StateUpdateTime))
hitObject.MoveToOffset(new Vector2(-100, 0));
}
}
[Test]
public void TestNotReusedWithHitObjectsSpacedClose()
{
@ -134,41 +171,44 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
[BackgroundDependencyLoader]
private void load()
{
RegisterPool<TestHitObject, DrawableTestHitObject>(PoolSize);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
public override DrawableHitObject<TestHitObject> CreateDrawableRepresentation(TestHitObject h) => null;
protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager();
protected override Playfield CreatePlayfield() => new TestPlayfield();
private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry
{
public TestHitObjectLifetimeEntry(HitObject hitObject)
: base(hitObject)
{
}
protected override double InitialLifetimeOffset => 0;
}
protected override Playfield CreatePlayfield() => new TestPlayfield(PoolSize);
}
private class TestPlayfield : Playfield
{
public TestPlayfield()
private readonly int poolSize;
public TestPlayfield(int poolSize)
{
this.poolSize = poolSize;
AddInternal(HitObjectContainer);
}
[BackgroundDependencyLoader]
private void load()
{
RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
protected override GameplayCursorContainer CreateCursor() => null;
}
private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry
{
public TestHitObjectLifetimeEntry(HitObject hitObject)
: base(hitObject)
{
}
protected override double InitialLifetimeOffset => 0;
}
private class TestBeatmapConverter : BeatmapConverter<TestHitObject>
{
public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
@ -207,7 +247,6 @@ namespace osu.Game.Tests.Visual.Gameplay
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200));
Size = new Vector2(50, 50);
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f);
@ -222,6 +261,12 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
protected override void OnApply(HitObject hitObject)
{
base.OnApply(hitObject);
Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200));
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (timeOffset > HitObject.Duration)

View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tournament.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tournament.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
}
]
}

View File

@ -144,9 +144,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
if (selected)
{
selectionBox.Show();
if (editor)
if (editor && editorInfo != null)
editorInfo.Selected.Value = Match;
else
else if (ladderInfo != null)
ladderInfo.CurrentMatch.Value = Match;
}
else

View File

@ -83,8 +83,8 @@ namespace osu.Game.Tournament.Screens
},
new ActionableInfo
{
Label = "Current User",
ButtonText = "Change Login",
Label = "Current user",
ButtonText = "Change sign-in",
Action = () =>
{
api.Logout();
@ -102,12 +102,12 @@ namespace osu.Game.Tournament.Screens
},
Value = api?.LocalUser.Value.Username,
Failing = api?.IsLoggedIn != true,
Description = "In order to access the API and display metadata, a login is required."
Description = "In order to access the API and display metadata, signing in is required."
},
new LabelledDropdown<RulesetInfo>
{
Label = "Ruleset",
Description = "Decides what stats are displayed and which ranks are retrieved for players",
Description = "Decides what stats are displayed and which ranks are retrieved for players.",
Items = rulesets.AvailableRulesets,
Current = LadderInfo.Ruleset,
},

View File

@ -127,10 +127,10 @@ namespace osu.Game.Tournament
new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen },
new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen },
new Separator(),
new ScreenButton(typeof(TeamIntroScreen)) { Text = "TeamIntro", RequestSelection = SetScreen },
new ScreenButton(typeof(TeamIntroScreen)) { Text = "Team Intro", RequestSelection = SetScreen },
new ScreenButton(typeof(SeedingScreen)) { Text = "Seeding", RequestSelection = SetScreen },
new Separator(),
new ScreenButton(typeof(MapPoolScreen)) { Text = "MapPool", RequestSelection = SetScreen },
new ScreenButton(typeof(MapPoolScreen)) { Text = "Map Pool", RequestSelection = SetScreen },
new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen },
new Separator(),
new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen },

View File

@ -11,6 +11,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
namespace osu.Game.Audio
@ -76,7 +77,7 @@ namespace osu.Game.Audio
/// <param name="source">The <see cref="IPreviewTrackOwner"/> which may be the owner of the <see cref="PreviewTrack"/>.</param>
public void StopAnyPlaying(IPreviewTrackOwner source)
{
if (CurrentTrack == null || CurrentTrack.Owner != source)
if (CurrentTrack == null || (CurrentTrack.Owner != null && CurrentTrack.Owner != source))
return;
CurrentTrack.Stop();
@ -86,11 +87,12 @@ namespace osu.Game.Audio
/// <summary>
/// Creates the <see cref="TrackManagerPreviewTrack"/>.
/// </summary>
protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TrackManagerPreviewTrack(beatmapSetInfo, trackStore);
protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) =>
new TrackManagerPreviewTrack(beatmapSetInfo, trackStore);
public class TrackManagerPreviewTrack : PreviewTrack
{
[Resolved]
[Resolved(canBeNull: true)]
public IPreviewTrackOwner Owner { get; private set; }
private readonly BeatmapSetInfo beatmapSetInfo;
@ -102,6 +104,12 @@ namespace osu.Game.Audio
this.trackManager = trackManager;
}
protected override void LoadComplete()
{
base.LoadComplete();
Logger.Log($"A {nameof(PreviewTrack)} was created without a containing {nameof(IPreviewTrackOwner)}. An owner should be added for correct behaviour.");
}
protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo?.OnlineBeatmapSetID}.mp3");
}

View File

@ -46,12 +46,15 @@ namespace osu.Game.Online
{
if (modelInfo.NewValue == null)
attachDownload(null);
else if (manager.IsAvailableLocally(modelInfo.NewValue))
else if (manager?.IsAvailableLocally(modelInfo.NewValue) == true)
State.Value = DownloadState.LocallyAvailable;
else
attachDownload(manager.GetExistingDownload(modelInfo.NewValue));
attachDownload(manager?.GetExistingDownload(modelInfo.NewValue));
}, true);
if (manager == null)
return;
managerDownloadBegan = manager.DownloadBegan.GetBoundCopy();
managerDownloadBegan.BindValueChanged(downloadBegan);
managerDownloadFailed = manager.DownloadFailed.GetBoundCopy();

View File

@ -248,7 +248,9 @@ namespace osu.Game.Online.Leaderboards
[BackgroundDependencyLoader]
private void load()
{
apiState.BindTo(api.State);
if (api != null)
apiState.BindTo(api.State);
apiState.BindValueChanged(onlineStateChanged, true);
}
@ -303,7 +305,7 @@ namespace osu.Game.Online.Leaderboards
PlaceholderState = PlaceholderState.NetworkFailure;
});
api.Queue(getScoresRequest);
api?.Queue(getScoresRequest);
});
}

View File

@ -194,6 +194,20 @@ namespace osu.Game
dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy")));
dependencies.CacheAs<ISkinSource>(SkinManager);
// needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo.
SkinManager.ItemRemoved.BindValueChanged(weakRemovedInfo =>
{
if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo))
{
Schedule(() =>
{
// check the removed skin is not the current user choice. if it is, switch back to default.
if (removedInfo.ID == SkinManager.CurrentSkinInfo.Value.ID)
SkinManager.CurrentSkinInfo.Value = SkinInfo.Default;
});
}
});
dependencies.CacheAs(API ??= new APIAccess(LocalConfig));
dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient());

View File

@ -30,7 +30,7 @@ namespace osu.Game.Overlays.AccountCreation
public override void OnEntering(IScreen last)
{
if (string.IsNullOrEmpty(api.ProvidedUsername))
if (string.IsNullOrEmpty(api?.ProvidedUsername))
{
this.FadeOut();
this.Push(new ScreenEntry());
@ -43,7 +43,7 @@ namespace osu.Game.Overlays.AccountCreation
[BackgroundDependencyLoader(true)]
private void load(OsuColour colours, OsuGame game, TextureStore textures)
{
if (string.IsNullOrEmpty(api.ProvidedUsername))
if (string.IsNullOrEmpty(api?.ProvidedUsername))
return;
InternalChildren = new Drawable[]

View File

@ -217,7 +217,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
private void performLogin()
{
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
api.Login(username.Text, password.Text);
api?.Login(username.Text, password.Text);
else
shakeSignIn.Shake();
}

View File

@ -76,7 +76,7 @@ namespace osu.Game
// a dialog may be blocking the execution for now.
if (checkForDialog(current)) return;
game.CloseAllOverlays(false);
game?.CloseAllOverlays(false);
// we may already be at the target screen type.
if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled)

View File

@ -18,14 +18,17 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModNightcore<TObject> : ModDoubleTime, IApplicableToDrawableRuleset<TObject>
where TObject : HitObject
public abstract class ModNightcore : ModDoubleTime
{
public override string Name => "Nightcore";
public override string Acronym => "NC";
public override IconUsage? Icon => OsuIcon.ModNightcore;
public override string Description => "Uguuuuuuuu...";
}
public abstract class ModNightcore<TObject> : ModNightcore, IApplicableToDrawableRuleset<TObject>
where TObject : HitObject
{
private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);

View File

@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
private HitObjectLifetimeEntry lifetimeEntry;
[Resolved(CanBeNull = true)]
private DrawableRuleset drawableRuleset { get; set; }
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
private Container<PausableSkinnableSound> samplesContainer;
@ -201,6 +201,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
// Copy any existing result from the entry (required for rewind / judgement revert).
Result = lifetimeEntry.Result;
}
else
LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
// Ensure this DHO has a result.
Result ??= CreateResult(HitObject.CreateJudgement())
@ -212,7 +214,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var h in HitObject.NestedHitObjects)
{
var drawableNested = drawableRuleset?.GetPooledDrawableRepresentation(h)
var drawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h)
?? CreateNestedHitObject(h)
?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");
@ -285,8 +287,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnFree(HitObject);
HitObject = null;
Result = null;
lifetimeEntry = null;
clearExistingStateTransforms();
hasHitObjectApplied = false;
}
@ -403,8 +408,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
double transformTime = HitObject.StartTime - InitialLifetimeOffset;
base.ApplyTransformsAt(double.MinValue, true);
base.ClearTransformsAfter(double.MinValue, true);
clearExistingStateTransforms();
using (BeginAbsoluteSequence(transformTime, true))
UpdateInitialTransforms();
@ -432,6 +436,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
PlaySamples();
}
private void clearExistingStateTransforms()
{
base.ApplyTransformsAt(double.MinValue, true);
base.ClearTransformsAfter(double.MinValue, true);
}
/// <summary>
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> for convenience.
@ -638,6 +648,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required.
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
/// <para>
/// Only has an effect if this <see cref="DrawableHitObject"/> is not being pooled.
/// For pooled <see cref="DrawableHitObject"/>s, use <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/> instead.
/// </para>
/// </remarks>
protected virtual double InitialLifetimeOffset => 10000;

View File

@ -81,10 +81,18 @@ namespace osu.Game.Rulesets
value |= LegacyMods.HardRock;
break;
case ModPerfect _:
value |= LegacyMods.Perfect;
break;
case ModSuddenDeath _:
value |= LegacyMods.SuddenDeath;
break;
case ModNightcore _:
value |= LegacyMods.Nightcore;
break;
case ModDoubleTime _:
value |= LegacyMods.DoubleTime;
break;
@ -100,6 +108,14 @@ namespace osu.Game.Rulesets
case ModFlashlight _:
value |= LegacyMods.Flashlight;
break;
case ModCinema _:
value |= LegacyMods.Cinema;
break;
case ModAutoplay _:
value |= LegacyMods.Autoplay;
break;
}
}

View File

@ -15,10 +15,7 @@ using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
@ -248,7 +245,7 @@ namespace osu.Game.Rulesets.UI
if (drawableRepresentation != null)
Playfield.Add(drawableRepresentation);
else
Playfield.Add(GetLifetimeEntry(hitObject));
Playfield.Add(hitObject);
}
/// <summary>
@ -260,15 +257,10 @@ namespace osu.Game.Rulesets.UI
/// <param name="hitObject">The <see cref="HitObject"/> to remove.</param>
public bool RemoveHitObject(TObject hitObject)
{
var entry = GetLifetimeEntry(hitObject);
// May have been newly-created by the above call - remove it anyway.
RemoveLifetimeEntry(hitObject);
if (Playfield.Remove(entry))
if (Playfield.Remove(hitObject))
return true;
// If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct removal.
// If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct drawable removal.
var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject);
if (drawableObject != null)
return Playfield.Remove(drawableObject);
@ -276,16 +268,6 @@ namespace osu.Game.Rulesets.UI
return false;
}
protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject)
{
if (!(hitObject is TObject tHitObject))
throw new InvalidOperationException($"Unexpected hitobject type: {hitObject.GetType().ReadableName()}");
return CreateLifetimeEntry(tHitObject);
}
protected virtual HitObjectLifetimeEntry CreateLifetimeEntry(TObject hitObject) => new HitObjectLifetimeEntry(hitObject);
public override void SetRecordTarget(Replay recordingReplay)
{
if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager))
@ -327,9 +309,8 @@ namespace osu.Game.Rulesets.UI
/// Creates a <see cref="DrawableHitObject{TObject}"/> to represent a <see cref="HitObject"/>.
/// </summary>
/// <remarks>
/// If this method returns <c>null</c>, then this <see cref="DrawableRuleset"/> will assume the requested <see cref="HitObject"/> type is being pooled,
/// and will instead attempt to retrieve the <see cref="DrawableHitObject"/>s at the point they should become alive via pools registered through
/// <see cref="DrawableRuleset.RegisterPool{TObject, TDrawable}(int, int?)"/> or <see cref="DrawableRuleset.RegisterPool{TObject, TDrawable}(DrawablePool{TDrawable})"/>.
/// If this method returns <c>null</c>, then this <see cref="DrawableRuleset"/> will assume the requested <see cref="HitObject"/> type is being pooled inside the <see cref="Playfield"/>,
/// and will instead attempt to retrieve the <see cref="DrawableHitObject"/>s at the point they should become alive via pools registered in the <see cref="Playfield"/>.
/// </remarks>
/// <param name="h">The <see cref="HitObject"/> to represent.</param>
/// <returns>The representing <see cref="DrawableHitObject{TObject}"/>.</returns>
@ -549,99 +530,6 @@ namespace osu.Game.Rulesets.UI
/// Invoked when the user requests to pause while the resume overlay is active.
/// </summary>
public abstract void CancelResume();
private readonly Dictionary<Type, IDrawablePool> pools = new Dictionary<Type, IDrawablePool>();
private readonly Dictionary<HitObject, HitObjectLifetimeEntry> lifetimeEntries = new Dictionary<HitObject, HitObjectLifetimeEntry>();
/// <summary>
/// Registers a default <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type (via <see cref="GetPooledDrawableRepresentation"/>).
/// </summary>
/// <param name="initialSize">The number of <see cref="DrawableHitObject"/>s to be initially stored in the pool.</param>
/// <param name="maximumSize">
/// The maximum number of <see cref="DrawableHitObject"/>s that can be stored in the pool.
/// If this limit is exceeded, every subsequent <see cref="DrawableHitObject"/> will be created anew instead of being retrieved from the pool,
/// until some of the existing <see cref="DrawableHitObject"/>s are returned to the pool.
/// </param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
/// <summary>
/// Registers a custom <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type (via <see cref="GetPooledDrawableRepresentation"/>).
/// </summary>
/// <param name="pool">The <see cref="DrawablePool{T}"/> to register.</param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool<TObject, TDrawable>([NotNull] DrawablePool<TDrawable> pool)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
{
pools[typeof(TObject)] = pool;
AddInternal(pool);
}
/// <summary>
/// Attempts to retrieve the poolable <see cref="DrawableHitObject"/> representation of a <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to retrieve the <see cref="DrawableHitObject"/> representation of.</param>
/// <returns>The <see cref="DrawableHitObject"/> representing <see cref="HitObject"/>, or <c>null</c> if no poolable representation exists.</returns>
[CanBeNull]
public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject)
{
if (!pools.TryGetValue(hitObject.GetType(), out var pool))
return null;
return (DrawableHitObject)pool.Get(d =>
{
var dho = (DrawableHitObject)d;
// If this is the first time this DHO is being used (not loaded), then apply the DHO mods.
// This is done before Apply() so that the state is updated once when the hitobject is applied.
if (!dho.IsLoaded)
{
foreach (var m in Mods.OfType<IApplicableToDrawableHitObjects>())
m.ApplyToDrawableHitObjects(dho.Yield());
}
dho.Apply(hitObject, GetLifetimeEntry(hitObject));
});
}
/// <summary>
/// Creates the <see cref="HitObjectLifetimeEntry"/> for a given <see cref="HitObject"/>.
/// </summary>
/// <remarks>
/// This may be overridden to provide custom lifetime control (e.g. via <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/>.
/// </remarks>
/// <param name="hitObject">The <see cref="HitObject"/> to create the entry for.</param>
/// <returns>The <see cref="HitObjectLifetimeEntry"/>.</returns>
[NotNull]
protected abstract HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject);
/// <summary>
/// Retrieves or creates the <see cref="HitObjectLifetimeEntry"/> for a given <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to retrieve or create the <see cref="HitObjectLifetimeEntry"/> for.</param>
/// <returns>The <see cref="HitObjectLifetimeEntry"/> for <paramref name="hitObject"/>.</returns>
[NotNull]
protected HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject)
{
if (lifetimeEntries.TryGetValue(hitObject, out var entry))
return entry;
return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject);
}
/// <summary>
/// Removes the <see cref="HitObjectLifetimeEntry"/> for a <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to remove the <see cref="HitObjectLifetimeEntry"/> for.</param>
internal void RemoveLifetimeEntry([NotNull] HitObject hitObject) => lifetimeEntries.Remove(hitObject);
}
public class BeatmapInvalidForRulesetException : ArgumentException

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.UI
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
[Resolved(CanBeNull = true)]
private DrawableRuleset drawableRuleset { get; set; }
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
public HitObjectContainer()
{
@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI
{
Debug.Assert(!drawableMap.ContainsKey(entry));
var drawable = drawableRuleset.GetPooledDrawableRepresentation(entry.HitObject);
var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject);
if (drawable == null)
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.UI
{
internal interface IPooledHitObjectProvider
{
/// <summary>
/// Attempts to retrieve the poolable <see cref="DrawableHitObject"/> representation of a <see cref="HitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to retrieve the <see cref="DrawableHitObject"/> representation of.</param>
/// <returns>The <see cref="DrawableHitObject"/> representing <see cref="HitObject"/>, or <c>null</c> if no poolable representation exists.</returns>
[CanBeNull]
DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject);
}
}

View File

@ -4,12 +4,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@ -17,7 +19,8 @@ using osuTK;
namespace osu.Game.Rulesets.UI
{
public abstract class Playfield : CompositeDrawable
[Cached(typeof(IPooledHitObjectProvider))]
public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider
{
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
@ -138,39 +141,6 @@ namespace osu.Game.Rulesets.UI
return false;
}
/// <summary>
/// Adds a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> to this <see cref="Playfield"/>.
/// </summary>
/// <param name="entry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the <see cref="HitObject"/>.</param>
public virtual void Add(HitObjectLifetimeEntry entry)
{
HitObjectContainer.Add(entry);
lifetimeEntryMap[entry.HitObject] = entry;
OnHitObjectAdded(entry.HitObject);
}
/// <summary>
/// Removes a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> from this <see cref="Playfield"/>.
/// </summary>
/// <param name="entry">The <see cref="HitObjectLifetimeEntry"/> controlling the lifetime of the <see cref="HitObject"/>.</param>
/// <returns>Whether the <see cref="HitObject"/> was successfully removed.</returns>
public virtual bool Remove(HitObjectLifetimeEntry entry)
{
if (HitObjectContainer.Remove(entry))
{
lifetimeEntryMap.Remove(entry.HitObject);
OnHitObjectRemoved(entry.HitObject);
return true;
}
bool removedFromNested = false;
if (nestedPlayfields.IsValueCreated)
removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(entry));
return removedFromNested;
}
/// <summary>
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="Playfield"/>.
/// </summary>
@ -246,6 +216,128 @@ namespace osu.Game.Rulesets.UI
/// </summary>
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
#region Pooling support
private readonly Dictionary<Type, IDrawablePool> pools = new Dictionary<Type, IDrawablePool>();
/// <summary>
/// Adds a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> to this <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject"></param>
public virtual void Add(HitObject hitObject)
{
var entry = CreateLifetimeEntry(hitObject);
lifetimeEntryMap[entry.HitObject] = entry;
HitObjectContainer.Add(entry);
OnHitObjectAdded(entry.HitObject);
}
/// <summary>
/// Removes a <see cref="HitObjectLifetimeEntry"/> for a pooled <see cref="HitObject"/> from this <see cref="Playfield"/>.
/// </summary>
/// <param name="hitObject"></param>
/// <returns>Whether the <see cref="HitObject"/> was successfully removed.</returns>
public virtual bool Remove(HitObject hitObject)
{
if (lifetimeEntryMap.Remove(hitObject, out var entry))
{
HitObjectContainer.Remove(entry);
OnHitObjectRemoved(hitObject);
return true;
}
bool removedFromNested = false;
if (nestedPlayfields.IsValueCreated)
removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(hitObject));
return removedFromNested;
}
/// <summary>
/// Creates the <see cref="HitObjectLifetimeEntry"/> for a given <see cref="HitObject"/>.
/// </summary>
/// <remarks>
/// This may be overridden to provide custom lifetime control (e.g. via <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/>.
/// </remarks>
/// <param name="hitObject">The <see cref="HitObject"/> to create the entry for.</param>
/// <returns>The <see cref="HitObjectLifetimeEntry"/>.</returns>
[NotNull]
protected virtual HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject) => new HitObjectLifetimeEntry(hitObject);
/// <summary>
/// Registers a default <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type.
/// </summary>
/// <param name="initialSize">The number of <see cref="DrawableHitObject"/>s to be initially stored in the pool.</param>
/// <param name="maximumSize">
/// The maximum number of <see cref="DrawableHitObject"/>s that can be stored in the pool.
/// If this limit is exceeded, every subsequent <see cref="DrawableHitObject"/> will be created anew instead of being retrieved from the pool,
/// until some of the existing <see cref="DrawableHitObject"/>s are returned to the pool.
/// </param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
/// <summary>
/// Registers a custom <see cref="DrawableHitObject"/> pool with this <see cref="DrawableRuleset"/> which is to be used whenever
/// <see cref="DrawableHitObject"/> representations are requested for the given <typeparamref name="TObject"/> type.
/// </summary>
/// <param name="pool">The <see cref="DrawablePool{T}"/> to register.</param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool<TObject, TDrawable>([NotNull] DrawablePool<TDrawable> pool)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
{
pools[typeof(TObject)] = pool;
AddInternal(pool);
}
DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject)
{
var lookupType = hitObject.GetType();
IDrawablePool pool;
// Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists.
if (!pools.TryGetValue(lookupType, out pool))
{
foreach (var (t, p) in pools)
{
if (!t.IsInstanceOfType(hitObject))
continue;
pools[lookupType] = pool = p;
break;
}
}
return (DrawableHitObject)pool?.Get(d =>
{
var dho = (DrawableHitObject)d;
// If this is the first time this DHO is being used (not loaded), then apply the DHO mods.
// This is done before Apply() so that the state is updated once when the hitobject is applied.
if (!dho.IsLoaded)
{
foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>())
m.ApplyToDrawableHitObjects(dho.Yield());
}
if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry))
lifetimeEntryMap[hitObject] = entry = CreateLifetimeEntry(hitObject);
dho.Apply(hitObject, entry);
});
}
#endregion
#region Editor logic
/// <summary>

View File

@ -342,8 +342,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
if (!blueprint.IsHovered) continue;
if (SelectionHandler.HandleSelectionRequested(blueprint, e))
return clickSelectionBegan = true;
return clickSelectionBegan = SelectionHandler.HandleSelectionRequested(blueprint, e);
}
return false;
@ -457,6 +456,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprint == null)
return false;
if (snapProvider == null)
return true;
Debug.Assert(movementBlueprintOriginalPosition != null);
HitObject draggedObject = movementBlueprint.HitObject;

View File

@ -110,7 +110,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
protected virtual void OnOperationBegan()
{
ChangeHandler.BeginChange();
ChangeHandler?.BeginChange();
}
/// <summary>
@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
protected virtual void OnOperationEnded()
{
ChangeHandler.EndChange();
ChangeHandler?.EndChange();
}
#region User Input Handling

View File

@ -96,7 +96,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (lastDragEvent != null)
OnDrag(lastDragEvent);
if (Composer != null)
if (Composer != null && timeline != null)
{
Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2;
Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2;

View File

@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit.Timing
controlPointGroups.BindCollectionChanged((sender, args) =>
{
table.ControlGroups = controlPointGroups;
changeHandler.SaveState();
changeHandler?.SaveState();
}, true);
}

View File

@ -59,6 +59,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components
{
scheduledFilterUpdate?.Cancel();
if (filter == null)
return;
filter.Value = new FilterCriteria
{
SearchString = Search.Current.Value ?? string.Empty,

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
@ -35,7 +36,8 @@ using osuTK;
namespace osu.Game.Screens.Play
{
public class Spectator : OsuScreen
[Cached(typeof(IPreviewTrackOwner))]
public class Spectator : OsuScreen, IPreviewTrackOwner
{
private readonly User targetUser;
@ -62,6 +64,9 @@ namespace osu.Game.Screens.Play
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
private Score score;
private readonly object scoreLock = new object();
@ -275,6 +280,7 @@ namespace osu.Game.Screens.Play
{
watchButton.Enabled.Value = false;
beatmapPanelContainer.Clear();
previewTrackManager.StopAnyPlaying(this);
}
private void attemptStart()
@ -326,7 +332,6 @@ namespace osu.Game.Screens.Play
{
if (state?.BeatmapID == null)
{
beatmapPanelContainer.Clear();
onlineBeatmap = null;
return;
}
@ -359,6 +364,12 @@ namespace osu.Game.Screens.Play
beatmaps.Download(onlineBeatmap);
}
public override bool OnExiting(IScreen next)
{
previewTrackManager.StopAnyPlaying(this);
return base.OnExiting(next);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@ -48,16 +48,6 @@ namespace osu.Game.Skinning
this.audio = audio;
this.legacyDefaultResources = legacyDefaultResources;
ItemRemoved.BindValueChanged(weakRemovedInfo =>
{
if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo))
{
// check the removed skin is not the current user choice. if it is, switch back to default.
if (removedInfo.ID == CurrentSkinInfo.Value.ID)
CurrentSkinInfo.Value = SkinInfo.Default;
}
});
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
CurrentSkin.ValueChanged += skin =>
{

View File

@ -9,11 +9,14 @@ using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
@ -21,6 +24,7 @@ using osu.Game.Users;
namespace osu.Game.Tests.Beatmaps
{
[HeadlessTest]
public abstract class HitObjectSampleTest : PlayerTestScene
{
protected abstract IResourceStore<byte[]> Resources { get; }
@ -44,7 +48,9 @@ namespace osu.Game.Tests.Beatmaps
private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore();
private SkinSourceDependencyContainer dependencies;
private IBeatmap currentTestBeatmap;
protected sealed override bool HasCustomSteps => true;
protected override bool Autoplay => true;
protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent)));
@ -54,6 +60,8 @@ namespace osu.Game.Tests.Beatmaps
protected sealed override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
=> new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio);
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
protected void CreateTestWithBeatmap(string filename)
{
CreateTest(() =>
@ -73,6 +81,9 @@ namespace osu.Game.Tests.Beatmaps
currentTestBeatmap.BeatmapInfo.Ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID);
});
});
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Stack.CurrentScreen is ResultsScreen);
}
protected void SetupSkins(string beatmapFile, string userFile)

View File

@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps
/// <returns></returns>
protected abstract Ruleset CreateRuleset();
protected void Test(LegacyMods legacyMods, Type[] expectedMods)
protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods)
{
var ruleset = CreateRuleset();
var mods = ruleset.ConvertFromLegacyMods(legacyMods).ToList();
@ -31,5 +31,15 @@ namespace osu.Game.Tests.Beatmaps
Assert.IsNotNull(mods.SingleOrDefault(mod => mod.GetType() == modType));
}
}
protected void TestToLegacy(LegacyMods expectedLegacyMods, Type[] providedModTypes)
{
var ruleset = CreateRuleset();
var modInstances = ruleset.GetAllMods()
.Where(mod => providedModTypes.Contains(mod.GetType()))
.ToArray();
var actualLegacyMods = ruleset.ConvertToLegacyMods(modInstances);
Assert.AreEqual(expectedLegacyMods, actualLegacyMods);
}
}
}