diff --git a/appveyor.yml b/appveyor.yml index 4dcaa7b45e..be1727e7d7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,7 +2,5 @@ clone_depth: 1 version: '{branch}-{build}' image: Previous Visual Studio 2017 test: off -install: - - cmd: git submodule update --init --recursive --depth=5 build_script: - cmd: PowerShell -Version 2.0 .\build.ps1 diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml new file mode 100644 index 0000000000..d36298175b --- /dev/null +++ b/appveyor_deploy.yml @@ -0,0 +1,10 @@ +clone_depth: 1 +version: '{build}' +image: Previous Visual Studio 2017 +test: off +skip_non_tags: true +build_script: + - cmd: PowerShell -Version 2.0 .\build.ps1 +deploy: + - provider: Environment + name: nuget diff --git a/osu.Android.props b/osu.Android.props index 96706f2bdc..46fd5424df 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -1,5 +1,7 @@ + Debug + AnyCPU bin\$(Configuration) 4 2.0 @@ -35,7 +37,7 @@ None True prompt - true + true false SdkOnly False @@ -49,7 +51,6 @@ osu.licenseheader - @@ -60,7 +61,7 @@ - - + + diff --git a/osu.Android.sln.DotSettings b/osu.Android.sln.DotSettings index 3f5bd9d34d..5a97fc7518 100644 --- a/osu.Android.sln.DotSettings +++ b/osu.Android.sln.DotSettings @@ -1,4 +1,4 @@ - + True True True @@ -167,6 +167,14 @@ WARNING <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile> Code Cleanup (peppy) + Required + Required + Required + Explicit + ExpressionBody + ExpressionBody + True + NEXT_LINE True True True @@ -176,12 +184,22 @@ True True NEXT_LINE + 1 + 1 + NEXT_LINE + MULTILINE NEXT_LINE + 1 + 1 True + NEXT_LINE NEVER NEVER + True False + True NEVER + False False True False @@ -189,6 +207,7 @@ True True False + False CHOP_IF_LONG True 200 diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 538aaf2d7a..2461351110 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -23,10 +23,10 @@ - + - + diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs new file mode 100644 index 0000000000..04e6dea376 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Tests.Beatmaps; + +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) })] + [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); + + protected override Ruleset CreateRuleset() => new CatchRuleset(); + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index c89cd95f36..720ef1db42 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osuTK.Graphics; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics.Textures; using osu.Game.Audio; @@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests { switch (component.LookupName) { - case "Gameplay/Catch/fruit-catcher-idle": + case "Gameplay/catch/fruit-catcher-idle": return new CatcherCustomSkin(); } @@ -99,8 +100,7 @@ namespace osu.Game.Rulesets.Catch.Tests public Texture GetTexture(string componentName) => throw new NotImplementedException(); - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => - throw new NotImplementedException(); + public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); } } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index a603d96201..7b8c699f2c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Tests { } + protected override bool Autoplay => true; + [Test] public void TestHyperDash() { diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index c527a81f51..36342024b0 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 5428b4eeb8..71d68ace94 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -46,6 +46,11 @@ namespace osu.Game.Rulesets.Catch else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new CatchModDoubleTime(); + if (mods.HasFlag(LegacyMods.Perfect)) + yield return new CatchModPerfect(); + else if (mods.HasFlag(LegacyMods.SuddenDeath)) + yield return new CatchModSuddenDeath(); + if (mods.HasFlag(LegacyMods.Autoplay)) yield return new CatchModAutoplay(); @@ -67,14 +72,8 @@ namespace osu.Game.Rulesets.Catch if (mods.HasFlag(LegacyMods.NoFail)) yield return new CatchModNoFail(); - if (mods.HasFlag(LegacyMods.Perfect)) - yield return new CatchModPerfect(); - if (mods.HasFlag(LegacyMods.Relax)) yield return new CatchModRelax(); - - if (mods.HasFlag(LegacyMods.SuddenDeath)) - yield return new CatchModSuddenDeath(); } public override IEnumerable GetModsFor(ModType type) diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 19a1b59752..a25d9cb67e 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -6,6 +6,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Objects { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index 00734810b3..dd4a58a5ef 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -50,6 +50,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable public Func CheckPosition; + public bool IsOnPlate; + + public override bool RemoveWhenNotAlive => IsOnPlate; + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (CheckPosition == null) return; @@ -71,11 +75,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable switch (state) { case ArmedState.Miss: - this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out).Expire(); + this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out); break; case ArmedState.Hit: - this.FadeOut().Expire(); + this.FadeOut(); break; } } diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 8dd00756f2..6c8515eb90 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Catch.Replays protected Replay Replay; + private CatchReplayFrame currentFrame; + public override Replay Generate() { // todo: add support for HT DT @@ -35,9 +37,6 @@ namespace osu.Game.Rulesets.Catch.Replays float lastPosition = 0.5f; double lastTime = 0; - // Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled - Replay.Frames.Add(new CatchReplayFrame(-100000, lastPosition)); - void moveToNext(CatchHitObject h) { float positionChange = Math.Abs(lastPosition - h.X); @@ -58,18 +57,18 @@ namespace osu.Game.Rulesets.Catch.Replays { //we are already in the correct range. lastTime = h.StartTime; - Replay.Frames.Add(new CatchReplayFrame(h.StartTime, lastPosition)); + addFrame(h.StartTime, lastPosition); return; } if (impossibleJump) { - Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); + addFrame(h.StartTime, h.X); } else if (h.HyperDash) { - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable, lastPosition)); - Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); + addFrame(h.StartTime - timeAvailable, lastPosition); + addFrame(h.StartTime, h.X); } else if (dashRequired) { @@ -81,16 +80,16 @@ namespace osu.Game.Rulesets.Catch.Replays float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable); //dash movement - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + 1, lastPosition, true)); - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition)); - Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); + addFrame(h.StartTime - timeAvailable + 1, lastPosition, true); + addFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition); + addFrame(h.StartTime, h.X); } else { double timeBefore = positionChange / movement_speed; - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeBefore, lastPosition)); - Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); + addFrame(h.StartTime - timeBefore, lastPosition); + addFrame(h.StartTime, h.X); } lastTime = h.StartTime; @@ -122,5 +121,16 @@ namespace osu.Game.Rulesets.Catch.Replays return Replay; } + + private void addFrame(double time, float? position = null, bool dashing = false) + { + // todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame. + if (Replay.Frames.Count == 0) + Replay.Frames.Add(new CatchReplayFrame(time - 1, position, false, null)); + + var last = currentFrame; + currentFrame = new CatchReplayFrame(time, position, dashing, last); + Replay.Frames.Add(currentFrame); + } } } diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs index 103aa6c3f1..22532bc9ec 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using osu.Framework.Input.StateChanges; using osu.Framework.MathUtils; using osu.Game.Replays; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Replays { } - protected override bool IsImportant(CatchReplayFrame frame) => frame.Position > 0; + protected override bool IsImportant(CatchReplayFrame frame) => frame.Actions.Any(); protected float? Position { @@ -38,21 +39,11 @@ namespace osu.Game.Rulesets.Catch.Replays { if (!Position.HasValue) return new List(); - var actions = new List(); - - if (CurrentFrame.Dashing) - actions.Add(CatchAction.Dash); - - if (Position.Value > CurrentFrame.Position) - actions.Add(CatchAction.MoveRight); - else if (Position.Value < CurrentFrame.Position) - actions.Add(CatchAction.MoveLeft); - return new List { new CatchReplayState { - PressedActions = actions, + PressedActions = CurrentFrame?.Actions ?? new List(), CatcherX = Position.Value }, }; diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index 1e88b35c3b..b41a5e0612 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Catch.UI; @@ -11,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Replays { public class CatchReplayFrame : ReplayFrame, IConvertibleReplayFrame { + public List Actions = new List(); + public float Position; public bool Dashing; @@ -18,17 +21,40 @@ namespace osu.Game.Rulesets.Catch.Replays { } - public CatchReplayFrame(double time, float? position = null, bool dashing = false) + public CatchReplayFrame(double time, float? position = null, bool dashing = false, CatchReplayFrame lastFrame = null) : base(time) { Position = position ?? -1; Dashing = dashing; + + if (Dashing) + Actions.Add(CatchAction.Dash); + + if (lastFrame != null) + { + if (Position > lastFrame.Position) + lastFrame.Actions.Add(CatchAction.MoveRight); + else if (Position < lastFrame.Position) + lastFrame.Actions.Add(CatchAction.MoveLeft); + } } - public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap) + public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { - Position = legacyFrame.Position.X / CatchPlayfield.BASE_WIDTH; - Dashing = legacyFrame.ButtonState == ReplayButtonState.Left1; + Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH; + Dashing = currentFrame.ButtonState == ReplayButtonState.Left1; + + if (Dashing) + Actions.Add(CatchAction.Dash); + + // this probably needs some cross-checking with osu-stable to ensure it is actually correct. + if (lastFrame is CatchReplayFrame lastCatchFrame) + { + if (Position > lastCatchFrame.Position) + lastCatchFrame.Actions.Add(CatchAction.MoveRight); + else if (Position < lastCatchFrame.Position) + Actions.Add(CatchAction.MoveLeft); + } } } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs similarity index 87% rename from osu.Game.Rulesets.Catch/Objects/CatchHitWindows.cs rename to osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs index 837662f5fe..ff793a372e 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitWindows.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Catch.Objects +namespace osu.Game.Rulesets.Catch.Scoring { public class CatchHitWindows : HitWindows { diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 99b22b2d56..18785d65ea 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -4,7 +4,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index ceda643335..56c8b33e02 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -69,10 +69,12 @@ namespace osu.Game.Rulesets.Catch.UI caughtFruit.RelativePositionAxes = Axes.None; caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(fruit.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); + caughtFruit.IsOnPlate = true; caughtFruit.Anchor = Anchor.TopCentre; caughtFruit.Origin = Anchor.Centre; caughtFruit.Scale *= 0.7f; + caughtFruit.LifetimeStart = caughtFruit.HitObject.StartTime; caughtFruit.LifetimeEnd = double.MaxValue; MovableCatcher.Add(caughtFruit); @@ -205,7 +207,8 @@ namespace osu.Game.Rulesets.Catch.UI AdditiveTarget.Add(additive); - additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint).Expire(); + additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); + additive.Expire(true); Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); } @@ -300,6 +303,7 @@ namespace osu.Game.Rulesets.Catch.UI { this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); + Trail &= Dashing; } } else @@ -406,6 +410,9 @@ namespace osu.Game.Rulesets.Catch.UI f.MoveToY(f.Y + 75, 750, Easing.InSine); f.FadeOut(750); + + // todo: this shouldn't exist once DrawableHitObject's ClearTransformsAfter overrides are repaired. + f.LifetimeStart = Time.Current; f.Expire(); } } @@ -436,10 +443,13 @@ namespace osu.Game.Rulesets.Catch.UI ExplodingFruitTarget.Add(fruit); } + fruit.ClearTransforms(); fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine).Then().MoveToY(fruit.Y + 50, 500, Easing.InSine); fruit.MoveToX(fruit.X + originalX * 6, 1000); fruit.FadeOut(750); + // todo: this shouldn't exist once DrawableHitObject's ClearTransformsAfter overrides are repaired. + fruit.LifetimeStart = Time.Current; fruit.Expire(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs new file mode 100644 index 0000000000..957743c5f1 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Tests.Beatmaps; + +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) })] + [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) })] + public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); + + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs index 20ac5eaa39..a5248c7712 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; @@ -12,8 +13,14 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests { [TestFixture] + [HeadlessTest] public class TestSceneAutoGeneration : OsuTestScene { + /// + /// The number of frames which are generated at the start of a replay regardless of hitobject content. + /// + private const int frame_offset = 1; + [Test] public void TestSingleNote() { @@ -26,11 +33,11 @@ namespace osu.Game.Rulesets.Mania.Tests var generated = new ManiaAutoGenerator(beatmap).Generate(); - Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); - Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); - Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); - Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released"); + Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); + Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released"); } [Test] @@ -47,11 +54,11 @@ namespace osu.Game.Rulesets.Mania.Tests var generated = new ManiaAutoGenerator(beatmap).Generate(); - Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); - Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); - Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); - Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released"); + Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released"); } [Test] @@ -67,11 +74,11 @@ namespace osu.Game.Rulesets.Mania.Tests var generated = new ManiaAutoGenerator(beatmap).Generate(); - Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); - Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); - Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); - Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); + Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); + Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); } [Test] @@ -89,11 +96,13 @@ namespace osu.Game.Rulesets.Mania.Tests var generated = new ManiaAutoGenerator(beatmap).Generate(); - Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); - Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); - Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); - Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); + Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames"); + + Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time"); + + Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); } [Test] @@ -110,15 +119,15 @@ namespace osu.Game.Rulesets.Mania.Tests var generated = new ManiaAutoGenerator(beatmap).Generate(); - Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames"); - Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); - Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect first note release time"); - Assert.AreEqual(2000, generated.Frames[3].Time, "Incorrect second note hit time"); - Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); - Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released"); - Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released"); + Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames"); + Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time"); + Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time"); + Assert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time"); + Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released"); } [Test] @@ -137,16 +146,16 @@ namespace osu.Game.Rulesets.Mania.Tests var generated = new ManiaAutoGenerator(beatmap).Generate(); - Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames"); - Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); - Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect first note release time"); - Assert.AreEqual(2000, generated.Frames[2].Time, "Incorrect second note hit time"); - Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); - Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); - Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key1), "Key1 has not been released"); - Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has been released"); - Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released"); + Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames"); + Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time"); + Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time"); + Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has been released"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released"); } [Test] @@ -164,14 +173,14 @@ namespace osu.Game.Rulesets.Mania.Tests var generated = new ManiaAutoGenerator(beatmap).Generate(); - Assert.IsTrue(generated.Frames.Count == 4, "Replay must have 4 frames"); - Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); - Assert.AreEqual(3000, generated.Frames[2].Time, "Incorrect second note press time + first note release time"); - Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect second note release time"); - Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released"); - Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key2), "Key2 has not been pressed"); - Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been released"); + Assert.IsTrue(generated.Frames.Count == frame_offset + 3, "Replay must have 3 generated frames"); + Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time"); + Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key2), "Key2 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been released"); } private bool checkContains(ReplayFrame frame, params ManiaAction[] actions) => actions.All(action => ((ManiaReplayFrame)frame).Actions.Contains(action)); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs new file mode 100644 index 0000000000..26a1b1b1ec --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class TestSceneHitExplosion : OsuTestScene + { + private ScrollingTestContainer scrolling; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableNote), + typeof(DrawableManiaHitObject), + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Child = scrolling = new ScrollingTestContainer(ScrollingDirection.Down) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Y, + Y = -0.25f, + Size = new Vector2(Column.COLUMN_WIDTH, NotePiece.NOTE_HEIGHT), + }; + + int runcount = 0; + + AddRepeatStep("explode", () => + { + runcount++; + + if (runcount % 15 > 12) + return; + + scrolling.AddRange(new Drawable[] + { + new HitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + }, 100); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index 031abb08e2..8dae5e6d84 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -11,6 +11,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; @@ -40,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Tests { Child = new FillFlowContainer { + Clock = new FramedClock(new ManualClock()), Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, @@ -62,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.Tests private Drawable createNoteDisplay(ScrollingDirection direction, int identifier, out DrawableNote hitObject) { - var note = new Note { StartTime = 999999999 }; + var note = new Note { StartTime = 0 }; note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); return new ScrollingTestContainer(direction) @@ -77,7 +79,7 @@ namespace osu.Game.Rulesets.Mania.Tests private Drawable createHoldNoteDisplay(ScrollingDirection direction, int identifier, out DrawableHoldNote hitObject) { - var note = new HoldNote { StartTime = 999999999, Duration = 5000 }; + var note = new HoldNote { StartTime = 0, Duration = 5000 }; note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); return new ScrollingTestContainer(direction) @@ -133,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Tests Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Both, Width = 1.25f, - Colour = Color4.Black.Opacity(0.5f) + Colour = Color4.Green.Opacity(0.5f) }, content = new Container { RelativeSizeAxes = Axes.Both } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs index 395e6daf0a..d5fd2808b8 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs @@ -66,6 +66,8 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.TopCentre)); AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.BottomCentre)); + AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[0], Anchor.TopCentre)); + AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.BottomCentre)); AddStep("flip direction", () => { @@ -75,10 +77,14 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.BottomCentre)); AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.TopCentre)); + AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[0], Anchor.BottomCentre)); + AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.TopCentre)); } private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor); + private bool barsInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor); + private void createNote() { foreach (var stage in stages) @@ -114,8 +120,7 @@ namespace osu.Game.Rulesets.Mania.Tests var obj = new BarLine { StartTime = Time.Current + 2000, - ControlPoint = new TimingControlPoint(), - BeatIndex = major ? 0 : 1 + Major = major, }; obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index af10d5e06e..09bf9241f2 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index b591f9da22..f5412dcfc5 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Configuration { base.InitialiseDefaults(); - Set(ManiaRulesetSetting.ScrollTime, 2250.0, 50.0, 10000.0, 50.0); + Set(ManiaRulesetSetting.ScrollTime, 1500.0, 50.0, 5000.0, 50.0); Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index d945abdb04..37cba1fd3c 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -11,9 +11,9 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Difficulty.Skills; using osu.Game.Rulesets.Mania.Mods; -using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Difficulty { @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty StarRating = difficultyValue(skills) * star_scaling_factor, Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)(hitWindows.Great / 2) / clockRate, + GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, Skills = skills }; } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0c4e7d4858..c74a292331 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -46,6 +46,11 @@ namespace osu.Game.Rulesets.Mania else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new ManiaModDoubleTime(); + if (mods.HasFlag(LegacyMods.Perfect)) + yield return new ManiaModPerfect(); + else if (mods.HasFlag(LegacyMods.SuddenDeath)) + yield return new ManiaModSuddenDeath(); + if (mods.HasFlag(LegacyMods.Autoplay)) yield return new ManiaModAutoplay(); @@ -97,14 +102,8 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlag(LegacyMods.NoFail)) yield return new ManiaModNoFail(); - if (mods.HasFlag(LegacyMods.Perfect)) - yield return new ManiaModPerfect(); - if (mods.HasFlag(LegacyMods.Random)) yield return new ManiaModRandom(); - - if (mods.HasFlag(LegacyMods.SuddenDeath)) - yield return new ManiaModSuddenDeath(); } public override IEnumerable GetModsFor(ModType type) diff --git a/osu.Game.Rulesets.Mania/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs index 4c644a8f09..0981b028b2 100644 --- a/osu.Game.Rulesets.Mania/Objects/BarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/BarLine.cs @@ -1,21 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Objects { - public class BarLine : ManiaHitObject + public class BarLine : ManiaHitObject, IBarLine { - /// - /// The control point which this bar line is part of. - /// - public TimingControlPoint ControlPoint; - - /// - /// The index of the beat which this bar line represents within the control point. - /// This is a "major" bar line if % == 0. - /// - public int BeatIndex; + public bool Major { get; set; } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index e9c352c97e..56bc797c7f 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -40,9 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Colour = new Color4(255, 204, 33, 255), }); - bool isMajor = barLine.BeatIndex % (int)barLine.ControlPoint.TimeSignature == 0; - - if (isMajor) + if (barLine.Major) { AddInternal(new EquilateralTriangle { @@ -65,10 +63,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }); } - if (!isMajor && barLine.BeatIndex % 2 == 1) + if (!barLine.Major) Alpha = 0.2f; } + protected override void UpdateInitialTransforms() + { + } + protected override void UpdateStateTransforms(ArmedState state) { } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index e5b114ca81..5bfa07bd14 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -51,11 +51,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables switch (state) { case ArmedState.Miss: - this.FadeOut(150, Easing.In).Expire(); + this.FadeOut(150, Easing.In); break; case ArmedState.Hit: - this.FadeOut(150, Easing.OutQuint).Expire(); + this.FadeOut(150, Easing.OutQuint); break; } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 2cd81104a3..8f353ae138 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = colour.NewValue.Lighten(1f).Opacity(0.6f), + Colour = colour.NewValue.Lighten(1f).Opacity(0.2f), Radius = 10, }; }, true); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs index bb33693783..4521af7dfb 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs @@ -18,8 +18,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces /// internal class NotePiece : Container, IHasAccentColour { - public const float NOTE_HEIGHT = 10; - private const float head_colour_height = 6; + public const float NOTE_HEIGHT = 12; private readonly IBindable direction = new Bindable(); @@ -39,8 +38,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces colouredBox = new Box { RelativeSizeAxes = Axes.X, - Height = head_colour_height, - Alpha = 0.2f + Height = NOTE_HEIGHT / 2, + Alpha = 0.1f } }; } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index d28d04b3c1..0c82cf7bbc 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -5,8 +5,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects { diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs index 6bb21633b6..d0125f8793 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs @@ -3,7 +3,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects { diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index 70720a926b..995e1516cb 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs @@ -3,7 +3,9 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.Mania.Objects.Types; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects { diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs deleted file mode 100644 index 5f2ceab48b..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Mania.Objects -{ - public class ManiaHitWindows : HitWindows - { - private static readonly IReadOnlyDictionary base_ranges = new Dictionary - { - { HitResult.Perfect, (44.8, 38.8, 27.8) }, - { HitResult.Great, (128, 98, 68) }, - { HitResult.Good, (194, 164, 134) }, - { HitResult.Ok, (254, 224, 194) }, - { HitResult.Meh, (302, 272, 242) }, - { HitResult.Miss, (376, 346, 316) }, - }; - - public override bool IsHitResultAllowed(HitResult result) => true; - - public override void SetDifficulty(double difficulty) - { - Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]); - Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); - Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); - Ok = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Ok]); - Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]); - Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]); - } - } -} diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 7b8bbc2095..2b336ca16d 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -47,9 +47,6 @@ namespace osu.Game.Rulesets.Mania.Replays public override Replay Generate() { - // Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled - Replay.Frames.Add(new ManiaReplayFrame(-100000, 0)); - var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); var actions = new List(); @@ -70,6 +67,10 @@ namespace osu.Game.Rulesets.Mania.Replays } } + // todo: can be removed once FramedReplayInputHandler correctly handles rewinding before first frame. + if (Replay.Frames.Count == 0) + Replay.Frames.Add(new ManiaReplayFrame(group.First().Time - 1)); + Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray())); } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index f7277d3669..70ba5cd938 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Replays Actions.AddRange(actions); } - public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap) + public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { // We don't need to fully convert, just create the converter var converter = new ManiaBeatmapConverter(beatmap); diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs new file mode 100644 index 0000000000..549f0f9214 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Scoring +{ + public class ManiaHitWindows : HitWindows + { + } +} diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 5caf08fb1e..49894a644c 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -4,7 +4,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 91dd236ab1..3d2a070b0f 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System.Linq; @@ -11,6 +11,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.UI.Scrolling; using osuTK; @@ -19,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.UI { public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour { - private const float column_width = 45; + public const float COLUMN_WIDTH = 80; private const float special_column_width = 70; /// @@ -41,10 +43,7 @@ namespace osu.Game.Rulesets.Mania.UI Index = index; RelativeSizeAxes = Axes.Y; - Width = column_width; - - Masking = true; - CornerRadius = 5; + Width = COLUMN_WIDTH; background = new ColumnBackground { RelativeSizeAxes = Axes.Both }; @@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.UI explosionContainer = new Container { Name = "Hit explosions", - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, } } }, @@ -90,6 +89,12 @@ namespace osu.Game.Rulesets.Mania.UI Bottom = dir.NewValue == ScrollingDirection.Down ? ManiaStage.HIT_TARGET_POSITION : 0, }; + explosionContainer.Padding = new MarginPadding + { + Top = dir.NewValue == ScrollingDirection.Up ? NotePiece.NOTE_HEIGHT / 2 : 0, + Bottom = dir.NewValue == ScrollingDirection.Down ? NotePiece.NOTE_HEIGHT / 2 : 0 + }; + keyArea.Anchor = keyArea.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; }, true); } @@ -108,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.UI isSpecial = value; - Width = isSpecial ? special_column_width : column_width; + Width = isSpecial ? special_column_width : COLUMN_WIDTH; } } @@ -163,9 +168,10 @@ namespace osu.Game.Rulesets.Mania.UI if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; - explosionContainer.Add(new HitExplosion(judgedObject) + explosionContainer.Add(new HitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick) { - Anchor = Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre + Anchor = Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre, + Origin = Anchor.Centre }); } diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs index 5ee78aa496..57241da564 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs @@ -35,7 +35,6 @@ namespace osu.Game.Rulesets.Mania.UI.Components { Name = "Background", RelativeSizeAxes = Axes.Both, - Alpha = 0.3f }, backgroundOverlay = new Box { @@ -82,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components if (!IsLoaded) return; - background.Colour = AccentColour; + background.Colour = AccentColour.Darken(5); var brightPoint = AccentColour.Opacity(0.6f); var dimPoint = AccentColour.Opacity(0); diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index a0d713067d..386bcbb724 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osuTK.Graphics; @@ -17,7 +18,6 @@ namespace osu.Game.Rulesets.Mania.UI.Components { public class ColumnHitObjectArea : CompositeDrawable, IHasAccentColour { - private const float hit_target_height = 10; private const float hit_target_bar_height = 2; private readonly IBindable direction = new Bindable(); @@ -32,7 +32,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components hitTargetBar = new Box { RelativeSizeAxes = Axes.X, - Height = hit_target_height, + Height = NotePiece.NOTE_HEIGHT, + Alpha = 0.6f, Colour = Color4.Black }, hitTargetLine = new Container diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index f26526fe70..d371c1f7a8 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -2,14 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Input; -using osu.Framework.MathUtils; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mania.Beatmaps; @@ -19,8 +16,8 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -45,33 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI public DrawableManiaRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) { - // Generate the bar lines - double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; - - var timingPoints = Beatmap.ControlPointInfo.TimingPoints; - var barLines = new List(); - - for (int i = 0; i < timingPoints.Count; i++) - { - TimingControlPoint point = timingPoints[i]; - - // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object - double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - point.BeatLength : lastObjectTime + point.BeatLength * (int)point.TimeSignature; - - int index = 0; - - for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++) - { - barLines.Add(new BarLine - { - StartTime = t, - ControlPoint = point, - BeatIndex = index - }); - } - } - - BarLines = barLines; + BarLines = new BarLineGenerator(Beatmap).BarLines; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs index 48470add8b..ccbff226a9 100644 --- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs @@ -1,16 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; using osu.Framework.MathUtils; -using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; -using osu.Game.Rulesets.Objects.Drawables; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI { @@ -18,51 +16,112 @@ namespace osu.Game.Rulesets.Mania.UI { public override bool RemoveWhenNotAlive => true; - private readonly CircularContainer circle; + private readonly CircularContainer largeFaint; + private readonly CircularContainer mainGlow1; - public HitExplosion(DrawableHitObject judgedObject) + public HitExplosion(Color4 objectColour, bool isSmall = false) { - bool isTick = judgedObject is DrawableHoldNoteTick; - - Origin = Anchor.Centre; - RelativeSizeAxes = Axes.X; - Y = NotePiece.NOTE_HEIGHT / 2; Height = NotePiece.NOTE_HEIGHT; // scale roughly in-line with visual appearance of notes - Scale = new Vector2(isTick ? 0.4f : 0.8f); + Scale = new Vector2(1f, 0.6f); - InternalChild = circle = new CircularContainer + if (isSmall) + Scale *= 0.5f; + + const float angle_variangle = 15; // should be less than 45 + + const float roundness = 80; + + const float initial_height = 10; + + var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1); + + InternalChildren = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - // we want our size to be very small so the glow dominates it. - Size = new Vector2(0.1f), - EdgeEffect = new EdgeEffectParameters + largeFaint = new CircularContainer { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.1f, judgedObject.AccentColour.Value, Color4.White, 0, 1), - Radius = 100, - }, - Child = new Box - { - Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - AlwaysPresent = true + Masking = true, + // we want our size to be very small so the glow dominates it. + Size = new Vector2(0.8f), + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), + Roundness = 160, + Radius = 200, + }, + }, + mainGlow1 = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), + Roundness = 20, + Radius = 50, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour, + Roundness = roundness, + Radius = 40, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour, + Roundness = roundness, + Radius = 40, + }, } }; } protected override void LoadComplete() { + const double duration = 200; + base.LoadComplete(); - circle.ResizeTo(circle.Size * new Vector2(4, 20), 1000, Easing.OutQuint); - this.FadeIn(16).Then().FadeOut(500, Easing.OutQuint); + largeFaint + .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) + .FadeOut(duration * 2); + mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint); + + this.FadeOut(duration, Easing.Out); Expire(true); } } diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs new file mode 100644 index 0000000000..495f2738b5 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Tests.Beatmaps; + +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) })] + [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); + + protected override Ruleset CreateRuleset() => new OsuRuleset(); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/approachcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/approachcircle@2x.png deleted file mode 100755 index db2f4a5730..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/approachcircle@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursor@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursor@2x.png deleted file mode 100755 index 75f9ba5ea6..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursor@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursormiddle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursormiddle@2x.png deleted file mode 100755 index ebf59c18ba..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursormiddle@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit0@2x.png deleted file mode 100644 index bdb2bcbc41..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit0@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100@2x.png deleted file mode 100644 index 7db8eb3124..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100k@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100k@2x.png deleted file mode 100644 index 206840e467..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit100k@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300@2x.png deleted file mode 100644 index 2c7c07852f..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300g@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300g@2x.png deleted file mode 100644 index 1ce746e3a4..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300g@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300k@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300k@2x.png deleted file mode 100755 index b0db9c00af..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit300k@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit50@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit50@2x.png deleted file mode 100644 index 94c09d263a..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hit50@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircle@2x.png deleted file mode 100755 index 6674616472..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircle@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircleoverlay@2x.png deleted file mode 100755 index 1f98c1697e..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/hitcircleoverlay@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-nd@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-nd@2x.png deleted file mode 100644 index 626fd91e38..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-nd@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-spec@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-spec@2x.png deleted file mode 100644 index 76fd9ab168..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb-spec@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb0@2x.png deleted file mode 100644 index 0a24a72808..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb0@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb1@2x.png deleted file mode 100644 index e99f076947..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb1@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb2@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb2@2x.png deleted file mode 100644 index cd36a0ae16..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb2@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb3@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb3@2x.png deleted file mode 100644 index f494bd3f51..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb3@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb4@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb4@2x.png deleted file mode 100644 index a5b19887d6..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb4@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb5@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb5@2x.png deleted file mode 100644 index 4bb01f0e88..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb5@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb6@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb6@2x.png deleted file mode 100644 index 859e0aa4c1..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb6@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb7@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb7@2x.png deleted file mode 100644 index 90efda0994..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb7@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb8@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb8@2x.png deleted file mode 100644 index fcdf4ed4a4..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb8@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb9@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb9@2x.png deleted file mode 100644 index c990cf0fe6..0000000000 Binary files a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/sliderb9@2x.png and /dev/null differ diff --git a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs index 29e5146ff1..38aac50df6 100644 --- a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs @@ -26,12 +26,12 @@ namespace osu.Game.Rulesets.Osu.Tests } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, SkinManager skinManager) { var dllStore = new DllResourceStore("osu.Game.Rulesets.Osu.Tests.dll"); metricsSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); - defaultSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/default_skin"), audio, false); + defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info); specialSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/special_skin"), audio, true); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs new file mode 100644 index 0000000000..685a51d208 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -0,0 +1,128 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; +using osu.Framework.Testing.Input; +using osu.Game.Audio; +using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneCursorTrail : OsuTestScene + { + [Test] + public void TestSmoothCursorTrail() + { + Container scalingContainer = null; + + createTest(() => scalingContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = new CursorTrail() + }); + + AddStep("set large scale", () => scalingContainer.Scale = new Vector2(10)); + } + + [Test] + public void TestLegacySmoothCursorTrail() + { + createTest(() => new LegacySkinContainer(false) + { + Child = new LegacyCursorTrail() + }); + } + + [Test] + public void TestLegacyDisjointCursorTrail() + { + createTest(() => new LegacySkinContainer(true) + { + Child = new LegacyCursorTrail() + }); + } + + private void createTest(Func createContent) => AddStep("create trail", () => + { + Clear(); + + Add(new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Child = new MovingCursorInputManager { Child = createContent?.Invoke() } + }); + }); + + [Cached(typeof(ISkinSource))] + private class LegacySkinContainer : Container, ISkinSource + { + private readonly bool disjoint; + + public LegacySkinContainer(bool disjoint) + { + this.disjoint = disjoint; + + RelativeSizeAxes = Axes.Both; + } + + public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException(); + + public Texture GetTexture(string componentName) + { + switch (componentName) + { + case "cursortrail": + var tex = new Texture(Texture.WhitePixel.TextureGL); + + if (disjoint) + tex.ScaleAdjust = 1 / 25f; + return tex; + + case "cursormiddle": + return disjoint ? null : Texture.WhitePixel; + } + + return null; + } + + public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); + + public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); + + public event Action SourceChanged; + } + + private class MovingCursorInputManager : ManualInputManager + { + public MovingCursorInputManager() + { + UseParentInput = false; + } + + protected override void Update() + { + base.Update(); + + const double spin_duration = 1000; + double currentTime = Time.Current; + + double angle = (currentTime % spin_duration) / spin_duration * 2 * Math.PI; + Vector2 rPos = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); + + MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos)); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index ebb6cd3a5a..aa170eae1e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -6,23 +6,68 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Testing.Input; using osu.Game.Rulesets.Osu.UI.Cursor; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] public class TestSceneGameplayCursor : SkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(CursorTrail) }; + public override IReadOnlyList RequiredTypes => new[] + { + typeof(OsuCursorContainer), + typeof(CursorTrail) + }; [BackgroundDependencyLoader] private void load() { - SetContents(() => new OsuCursorContainer + SetContents(() => new MovingCursorInputManager { - RelativeSizeAxes = Axes.Both, - Masking = true, + Child = new ClickingCursorContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + } }); } + + private class ClickingCursorContainer : OsuCursorContainer + { + protected override void Update() + { + base.Update(); + + double currentTime = Time.Current; + + if (((int)(currentTime / 1000)) % 2 == 0) + OnPressed(OsuAction.LeftButton); + else + OnReleased(OsuAction.LeftButton); + } + } + + private class MovingCursorInputManager : ManualInputManager + { + public MovingCursorInputManager() + { + UseParentInput = false; + } + + protected override void Update() + { + base.Update(); + + const double spin_duration = 5000; + double currentTime = Time.Current; + + double angle = (currentTime % spin_duration) / spin_duration * 2 * Math.PI; + Vector2 rPos = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); + + MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos)); + } + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs index 399cf22599..95c2810e94 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs @@ -29,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Tests }; for (int i = 0; i < 512; i++) - beatmap.HitObjects.Add(new HitCircle { Position = new Vector2(256, 192), StartTime = i * 100 }); + if (i % 32 < 20) + beatmap.HitObjects.Add(new HitCircle { Position = new Vector2(256, 192), StartTime = i * 100 }); return beatmap; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs index 585fdb9cb4..863d0eda09 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Tests Debug.Assert(drawableHitObject.HitObject.HitWindows != null); - double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.HalfWindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current; + double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current; Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay); return drawableHitObject; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index fe73e7c861..02c65db6ad 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -135,6 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests public SampleChannel GetSample(ISampleInfo sampleInfo) => null; public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default; + public IBindable GetConfig(TLookup lookup) => null; public event Action SourceChanged; diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index c331c811d2..791043bcc6 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 61e9f60cdd..b0d261a1cc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -9,11 +9,12 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Difficulty { @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future - double hitWindowGreat = (int)(hitWindows.Great / 2) / clockRate; + double hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate; double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; int maxCombo = beatmap.HitObjects.Count; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index ca72f18e9c..65d7acc911 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) }; public bool AllowFail => false; + public bool RestartOnFail => false; private OsuInputManager inputManager; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 2d940479f3..32c9e913c6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -17,7 +17,9 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => 1.06; - public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn) }; + + public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable), typeof(OsuModSpinIn) }; + private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index 62b5ecfd58..e786ec86f9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; // todo: this mod should be able to be compatible with hidden with a bit of further implementation. - public override Type[] IncompatibleMods => new[] { typeof(OsuModeObjectScaleTween), typeof(OsuModHidden) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModeObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) }; private const int rotate_offset = 360; private const float rotate_starting_width = 2; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs new file mode 100644 index 0000000000..7e20feba02 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . 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.Bindables; +using System.Collections.Generic; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Mods +{ + internal class OsuModTraceable : Mod, IReadFromConfig, IApplicableToDrawableHitObjects + { + public override string Name => "Traceable"; + public override string Acronym => "TC"; + public override IconUsage Icon => FontAwesome.Brands.SnapchatGhost; + public override ModType Type => ModType.Fun; + public override string Description => "Put your faith in the approach circles..."; + public override double ScoreMultiplier => 1; + + public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModeObjectScaleTween) }; + private Bindable increaseFirstObjectVisibility = new Bindable(); + + public void ReadFromConfig(OsuConfigManager config) + { + increaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); + } + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0)) + drawable.ApplyCustomUpdateState += ApplyTraceableState; + } + + protected void ApplyTraceableState(DrawableHitObject drawable, ArmedState state) + { + if (!(drawable is DrawableOsuHitObject drawableOsu)) + return; + + var h = drawableOsu.HitObject; + + switch (drawable) + { + case DrawableHitCircle circle: + // we only want to see the approach circle + using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + circle.CirclePiece.Hide(); + + break; + + case DrawableSlider slider: + slider.AccentColour.BindValueChanged(_ => + { + //will trigger on skin change. + slider.Body.AccentColour = slider.AccentColour.Value.Opacity(0); + slider.Body.BorderColour = slider.AccentColour.Value; + }, true); + + break; + + case DrawableSpinner spinner: + spinner.Disc.Hide(); + spinner.Background.Hide(); + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs index e926ade41b..923278f484 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Mods private Bindable increaseFirstObjectVisibility = new Bindable(); - public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) }; public void ReadFromConfig(OsuConfigManager config) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3decc4e51f..c90f230f93 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -10,15 +10,15 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using osuTK; using osu.Game.Rulesets.Scoring; +using osuTK; using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach { - public ApproachCircle ApproachCircle; + public ApproachCircle ApproachCircle { get; } private readonly IBindable positionBindable = new Bindable(); private readonly IBindable stackHeightBindable = new Bindable(); @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly HitArea hitArea; - private readonly SkinnableDrawable mainContent; + public SkinnableDrawable CirclePiece { get; } public DrawableHitCircle(HitCircle h) : base(h) @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return true; }, }, - mainContent = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece(HitObject.IndexInCurrentCombo)), + CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece(HitObject.IndexInCurrentCombo)), ApproachCircle = new ApproachCircle { Alpha = 0, @@ -86,6 +86,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true); } + public override double LifetimeStart + { + get => base.LifetimeStart; + set + { + base.LifetimeStart = value; + ApproachCircle.LifetimeStart = value; + } + } + + public override double LifetimeEnd + { + get => base.LifetimeEnd; + set + { + base.LifetimeEnd = value; + ApproachCircle.LifetimeEnd = value; + } + } + protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); @@ -102,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (result == HitResult.None) { - Shake(Math.Abs(timeOffset) - HitObject.HitWindows.HalfWindowFor(HitResult.Miss)); + Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss)); return; } @@ -113,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateInitialTransforms(); - mainContent.FadeInFromZero(HitObject.TimeFadeIn); + CirclePiece.FadeInFromZero(HitObject.TimeFadeIn); ApproachCircle.FadeIn(Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt)); ApproachCircle.ScaleTo(1f, HitObject.TimePreempt); @@ -122,6 +142,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateStateTransforms(ArmedState state) { + base.UpdateStateTransforms(state); + Debug.Assert(HitObject.HitWindows != null); switch (state) @@ -132,22 +154,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Expire(true); hitArea.HitAction = null; - - // override lifetime end as FadeIn may have been changed externally, causing out expiration to be too early. - LifetimeEnd = HitObject.StartTime + HitObject.HitWindows.HalfWindowFor(HitResult.Miss); break; case ArmedState.Miss: ApproachCircle.FadeOut(50); this.FadeOut(100); - Expire(); break; case ArmedState.Hit: ApproachCircle.FadeOut(50); // todo: temporary / arbitrary - this.Delay(800).Expire(); + this.Delay(800).FadeOut(); break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index fcd42314fc..c46343c73c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -41,6 +41,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); + protected override void UpdateStateTransforms(ArmedState state) + { + base.UpdateStateTransforms(state); + + switch (state) + { + case ArmedState.Idle: + // Manually set to reduce the number of future alive objects to a bare minimum. + LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; + break; + } + } + protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 938a2293ba..022e9ea12b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -1,22 +1,66 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Configuration; using osuTK; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuJudgement : DrawableJudgement { + private SkinnableSprite lighting; + private Bindable lightingColour; + public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject) : base(result, judgedObject) { } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + if (config.Get(OsuSetting.HitLighting) && Result.Type != HitResult.Miss) + { + AddInternal(lighting = new SkinnableSprite("lighting") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + Depth = float.MaxValue + }); + + if (JudgedObject != null) + { + lightingColour = JudgedObject.AccentColour.GetBoundCopy(); + lightingColour.BindValueChanged(colour => lighting.Colour = colour.NewValue, true); + } + else + { + lighting.Colour = Color4.White; + } + } + } + + protected override double FadeOutDelay => lighting == null ? base.FadeOutDelay : 1400; + protected override void ApplyHitAnimations() { + if (lighting != null) + { + JudgementBody.Delay(FadeInDuration).FadeOut(400); + + lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); + lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); + } + JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); base.ApplyHitAnimations(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index 50187781f6..84d2a4af9b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -9,8 +9,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Drawables; -using osuTK; using osu.Game.Rulesets.Scoring; +using osuTK; using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -74,6 +74,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateStateTransforms(ArmedState state) { + base.UpdateStateTransforms(state); + switch (state) { case ArmedState.Idle: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 1749ea1f60..9e8ad9851c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -12,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -162,16 +163,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private float sliderPathRadius; - protected override void SkinChanged(ISkinSource skin, bool allowFallback) + protected override void ApplySkin(ISkinSource skin, bool allowFallback) { - base.SkinChanged(skin, allowFallback); + base.ApplySkin(skin, allowFallback); - Body.BorderSize = skin.GetValue(s => s.SliderBorderSize) ?? SliderBody.DEFAULT_BORDER_SIZE; - sliderPathRadius = skin.GetValue(s => s.SliderPathRadius) ?? OsuHitObject.OBJECT_RADIUS; + Body.BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? SliderBody.DEFAULT_BORDER_SIZE; + sliderPathRadius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; updatePathRadius(); - Body.AccentColour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderTrackOverride") ? s.CustomColours["SliderTrackOverride"] : (Color4?)null) ?? AccentColour.Value; - Body.BorderColour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderBorder") ? s.CustomColours["SliderBorder"] : (Color4?)null) ?? Color4.White; + Body.AccentColour = skin.GetConfig(OsuSkinColour.SliderTrackOverride)?.Value ?? AccentColour.Value; + Body.BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; + + bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; + Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White; } private void updatePathRadius() => Body.PathRadius = slider.Scale * sliderPathRadius; @@ -201,6 +205,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateStateTransforms(ArmedState state) { + base.UpdateStateTransforms(state); + Ball.FadeIn(); Ball.ScaleTo(HitObject.Scale); @@ -218,10 +224,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; } - this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); + this.FadeOut(fade_out_time, Easing.OutQuint); } - - Expire(true); } public Drawable ProxiedLayer => HeadCircle.ApproachCircle; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index c5fa5f0af5..9d4d9958a1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -8,9 +8,9 @@ using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -75,6 +75,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateStateTransforms(ArmedState state) { + base.UpdateStateTransforms(state); + switch (state) { case ArmedState.Idle: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a0bd301fdb..d1b9ee6cb4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -13,8 +13,8 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; -using osu.Game.Screens.Ranking; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Ranking; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -215,14 +215,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateStateTransforms(ArmedState state) { + base.UpdateStateTransforms(state); + var sequence = this.Delay(Spinner.Duration).FadeOut(160); switch (state) { - case ArmedState.Idle: - Expire(true); - break; - case ArmedState.Hit: sequence.ScaleTo(Scale * 1.2f, 320, Easing.Out); break; @@ -231,8 +229,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables sequence.ScaleTo(Scale * 0.8f, 320, Easing.In); break; } - - Expire(); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 7c871c6ccd..ef7b077480 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -11,6 +11,7 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Skinning; using osuTK.Graphics; using osu.Game.Skinning; using osuTK; @@ -218,7 +219,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { RelativeSizeAxes = Axes.Both; - float radius = skin.GetValue(s => s.SliderPathRadius) ?? OsuHitObject.OBJECT_RADIUS; + float radius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; InternalChild = new CircularContainer { diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index b52bfcd181..2cf877b000 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -7,6 +7,8 @@ using osu.Game.Rulesets.Objects; using osuTK; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs deleted file mode 100644 index add8fd53c7..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Osu.Objects -{ - public class OsuHitWindows : HitWindows - { - private static readonly IReadOnlyDictionary base_ranges = new Dictionary - { - { HitResult.Great, (160, 100, 40) }, - { HitResult.Good, (280, 200, 120) }, - { HitResult.Meh, (400, 300, 200) }, - { HitResult.Miss, (400, 400, 400) }, - }; - - public override void SetDifficulty(double difficulty) - { - Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); - Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); - Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]); - Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]); - } - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs index 63713541b4..a794e57c9e 100644 --- a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs @@ -6,6 +6,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { @@ -28,5 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects } public override Judgement CreateJudgement() => new OsuJudgement(); + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 93231844bb..2805494021 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 4f2af64161..7e540a577b 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { @@ -23,5 +24,7 @@ namespace osu.Game.Rulesets.Osu.Objects } public override Judgement CreateJudgement() => new OsuSliderTailJudgement(); + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 60e9084ed3..af7cf5b144 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -4,8 +4,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 69c779a182..2e7b763966 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -6,8 +6,8 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 27899ab56e..fa69cec78d 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -52,6 +52,11 @@ namespace osu.Game.Rulesets.Osu else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new OsuModDoubleTime(); + if (mods.HasFlag(LegacyMods.Perfect)) + yield return new OsuModPerfect(); + else if (mods.HasFlag(LegacyMods.SuddenDeath)) + yield return new OsuModSuddenDeath(); + if (mods.HasFlag(LegacyMods.Autopilot)) yield return new OsuModAutopilot(); @@ -76,18 +81,12 @@ namespace osu.Game.Rulesets.Osu if (mods.HasFlag(LegacyMods.NoFail)) yield return new OsuModNoFail(); - if (mods.HasFlag(LegacyMods.Perfect)) - yield return new OsuModPerfect(); - if (mods.HasFlag(LegacyMods.Relax)) yield return new OsuModRelax(); if (mods.HasFlag(LegacyMods.SpunOut)) yield return new OsuModSpunOut(); - if (mods.HasFlag(LegacyMods.SuddenDeath)) - yield return new OsuModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Target)) yield return new OsuModTarget(); @@ -140,6 +139,7 @@ namespace osu.Game.Rulesets.Osu new OsuModSpinIn(), new MultiMod(new OsuModGrow(), new OsuModDeflate()), new MultiMod(new ModWindUp(), new ModWindDown()), + new OsuModTraceable(), }; case ModType.System: @@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Osu public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this); - public override ISkin CreateLegacySkinProvider(ISkinSource source) => new OsuLegacySkin(source); + public override ISkin CreateLegacySkinProvider(ISkinSource source) => new OsuLegacySkinTransformer(source); public override int? LegacyID => 0; diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 5971f053c2..8dd48eace0 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Osu HitCircle, FollowPoint, Cursor, + CursorTrail, SliderScorePoint, ApproachCircle, ReverseArrow, diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index e5fa571d4d..24320b6579 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -10,9 +10,9 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; using osu.Game.Replays; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Replays @@ -118,29 +118,29 @@ namespace osu.Game.Rulesets.Osu.Replays Debug.Assert(hitWindows != null); // Make the cursor stay at a hitObject as long as possible (mainly for autopilot). - if (h.StartTime - hitWindows.HalfWindowFor(HitResult.Miss) > endTime + hitWindows.HalfWindowFor(HitResult.Meh) + 50) + if (h.StartTime - hitWindows.WindowFor(HitResult.Miss) > endTime + hitWindows.WindowFor(HitResult.Meh) + 50) { if (!(prev is Spinner) && h.StartTime - endTime < 1000) - AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.HalfWindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); if (!(h is Spinner)) - AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.HalfWindowFor(HitResult.Miss), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Miss), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } - else if (h.StartTime - hitWindows.HalfWindowFor(HitResult.Meh) > endTime + hitWindows.HalfWindowFor(HitResult.Meh) + 50) + else if (h.StartTime - hitWindows.WindowFor(HitResult.Meh) > endTime + hitWindows.WindowFor(HitResult.Meh) + 50) { if (!(prev is Spinner) && h.StartTime - endTime < 1000) - AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.HalfWindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); if (!(h is Spinner)) - AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.HalfWindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } - else if (h.StartTime - hitWindows.HalfWindowFor(HitResult.Good) > endTime + hitWindows.HalfWindowFor(HitResult.Good) + 50) + else if (h.StartTime - hitWindows.WindowFor(HitResult.Good) > endTime + hitWindows.WindowFor(HitResult.Good) + 50) { if (!(prev is Spinner) && h.StartTime - endTime < 1000) - AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.HalfWindowFor(HitResult.Good), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Good), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); if (!(h is Spinner)) - AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.HalfWindowFor(HitResult.Good), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Good), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs index 4d90fcadd5..e6c6db5e61 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs @@ -26,11 +26,11 @@ namespace osu.Game.Rulesets.Osu.Replays Actions.AddRange(actions); } - public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap) + public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { - Position = legacyFrame.Position; - if (legacyFrame.MouseLeft) Actions.Add(OsuAction.LeftButton); - if (legacyFrame.MouseRight) Actions.Add(OsuAction.RightButton); + Position = currentFrame.Position; + if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton); + if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton); } } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs new file mode 100644 index 0000000000..a6491bb3f3 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public class OsuHitWindows : HitWindows + { + private static readonly DifficultyRange[] osu_ranges = + { + new DifficultyRange(HitResult.Great, 80, 50, 20), + new DifficultyRange(HitResult.Good, 140, 100, 60), + new DifficultyRange(HitResult.Meh, 200, 150, 100), + new DifficultyRange(HitResult.Miss, 200, 200, 200), + }; + + public override bool IsHitResultAllowed(HitResult result) + { + switch (result) + { + case HitResult.Great: + case HitResult.Good: + case HitResult.Meh: + case HitResult.Miss: + return true; + } + + return false; + } + + protected override DifficultyRange[] GetRanges() => osu_ranges; + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 66ef020d09..affe18a30d 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -22,8 +20,6 @@ namespace osu.Game.Rulesets.Osu.Scoring private float hpDrainRate; - private readonly Dictionary comboResultCounts = new Dictionary(); - protected override void ApplyBeatmap(Beatmap beatmap) { base.ApplyBeatmap(beatmap); @@ -31,22 +27,6 @@ namespace osu.Game.Rulesets.Osu.Scoring hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; } - protected override void Reset(bool storeResults) - { - base.Reset(storeResults); - comboResultCounts.Clear(); - } - - protected override void ApplyResult(JudgementResult result) - { - base.ApplyResult(result); - - var osuResult = (OsuJudgementResult)result; - - if (result.Type != HitResult.None) - comboResultCounts[osuResult.ComboType] = comboResultCounts.GetOrDefault(osuResult.ComboType) + 1; - } - protected override double HealthAdjustmentFactorFor(JudgementResult result) { switch (result.Type) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs new file mode 100644 index 0000000000..1885c76fcc --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public class LegacyCursorTrail : CursorTrail + { + private const double disjoint_trail_time_separation = 1000 / 60.0; + + private bool disjointTrail; + private double lastTrailTime; + + public LegacyCursorTrail() + { + Blending = BlendingParameters.Additive; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + Texture = skin.GetTexture("cursortrail"); + disjointTrail = skin.GetTexture("cursormiddle") == null; + + if (Texture != null) + { + // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation. + Texture.ScaleAdjust *= 1.6f; + } + } + + protected override double FadeDuration => disjointTrail ? 150 : 500; + + protected override bool InterpolateMovements => !disjointTrail; + + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (!disjointTrail) + return base.OnMouseMove(e); + + if (Time.Current - lastTrailTime >= disjoint_trail_time_separation) + { + lastTrailTime = Time.Current; + return base.OnMouseMove(e); + } + + return false; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index ec838c596d..81c02199d0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) { - animationContent.Colour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderBall") ? s.CustomColours["SliderBall"] : (Color4?)null) ?? Color4.White; + animationContent.Colour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; InternalChildren = new[] { diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkin.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs similarity index 64% rename from osu.Game.Rulesets.Osu/Skinning/OsuLegacySkin.cs rename to osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index e3e302b81c..479c250eab 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkin.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -3,21 +3,19 @@ using System; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Skinning; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning { - public class OsuLegacySkin : ISkin + public class OsuLegacySkinTransformer : ISkin { private readonly ISkin source; - private Lazy configuration; - private Lazy hasHitCircle; /// @@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Skinning /// private const float legacy_circle_radius = 64 - 5; - public OsuLegacySkin(ISkinSource source) + public OsuLegacySkinTransformer(ISkinSource source) { this.source = source; @@ -37,21 +35,6 @@ namespace osu.Game.Rulesets.Osu.Skinning private void sourceChanged() { - // these need to be lazy in order to ensure they aren't called before the dependencies have been loaded into our source. - configuration = new Lazy(() => - { - var config = new SkinConfiguration(); - if (hasHitCircle.Value) - config.SliderPathRadius = legacy_circle_radius; - - // defaults should only be applied for non-beatmap skins (which are parsed via this constructor). - config.CustomColours["SliderBall"] = - source.GetValue(s => s.CustomColours.TryGetValue("SliderBall", out var val) ? val : (Color4?)null) - ?? new Color4(2, 170, 255, 255); - - return config; - }); - hasHitCircle = new Lazy(() => source.GetTexture("hitcircle") != null); } @@ -62,6 +45,9 @@ namespace osu.Game.Rulesets.Osu.Skinning switch (osuComponent.Component) { + case OsuSkinComponents.FollowPoint: + return this.GetAnimation(component.LookupName, true, false); + case OsuSkinComponents.SliderFollowCircle: return this.GetAnimation("sliderfollowcircle", true, true); @@ -95,17 +81,23 @@ namespace osu.Game.Rulesets.Osu.Skinning return null; + case OsuSkinComponents.CursorTrail: + if (source.GetTexture("cursortrail") != null) + return new LegacyCursorTrail(); + + return null; + case OsuSkinComponents.HitCircleText: - string font = GetValue(config => config.HitCircleFont); - var overlap = GetValue(config => config.HitCircleOverlap); + var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; + var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0; return !hasFont(font) ? null : new LegacySpriteText(source, font) { - // Spacing value was reverse-engineered from the ratio of the rendered sprite size in the visual inspector vs the actual texture size - Scale = new Vector2(0.96f), - Spacing = new Vector2(-overlap * 0.89f, 0) + // stable applies a blanket 0.8x scale to hitcircle fonts + Scale = new Vector2(0.8f), + Spacing = new Vector2(-overlap, 0) }; } @@ -116,13 +108,27 @@ namespace osu.Game.Rulesets.Osu.Skinning public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample); - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration + public IBindable GetConfig(TLookup lookup) { - TValue val; - if (configuration.Value is TConfiguration conf && (val = query.Invoke(conf)) != null) - return val; + switch (lookup) + { + case OsuSkinColour colour: + return source.GetConfig(new SkinCustomColourLookup(colour)); - return source.GetValue(query); + case OsuSkinConfiguration osuLookup: + switch (osuLookup) + { + case OsuSkinConfiguration.SliderPathRadius: + if (hasHitCircle.Value) + return SkinUtils.As(new BindableFloat(legacy_circle_radius)); + + break; + } + + break; + } + + return source.GetConfig(lookup); } private bool hasFont(string fontName) => source.GetTexture($"{fontName}-0") != null; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs new file mode 100644 index 0000000000..4e6d3ef0e4 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public enum OsuSkinColour + { + SliderTrackOverride, + SliderBorder, + SliderBall + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs new file mode 100644 index 0000000000..98219cafe8 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public enum OsuSkinConfiguration + { + HitCirclePrefix, + HitCircleOverlap, + SliderBorderSize, + SliderPathRadius, + AllowSliderBallTint, + CursorExpand, + } +} diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 05eb0ffdbf..b32dfd483f 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; using osu.Framework.Allocation; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Batches; using osu.Framework.Graphics.OpenGL.Vertices; @@ -20,30 +21,15 @@ using osuTK.Graphics.ES30; namespace osu.Game.Rulesets.Osu.UI.Cursor { - internal class CursorTrail : Drawable, IRequireHighFrequencyMousePosition + public class CursorTrail : Drawable, IRequireHighFrequencyMousePosition { - private int currentIndex; - - private IShader shader; - private Texture texture; - - private Vector2 size => texture.Size * Scale; - - private double timeOffset; - - private float time; - - public override bool IsPresent => true; - private const int max_sprites = 2048; private readonly TrailPart[] parts = new TrailPart[max_sprites]; - - private Vector2? lastPosition; - - private readonly InputResampler resampler = new InputResampler(); - - protected override DrawNode CreateDrawNode() => new TrailDrawNode(this); + private int currentIndex; + private IShader shader; + private double timeOffset; + private float time; public CursorTrail() { @@ -60,14 +46,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - [BackgroundDependencyLoader] - private void load(ShaderManager shaders, TextureStore textures) + private void load(ShaderManager shaders) { shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); - texture = textures.Get(@"Cursor/cursortrail"); - Scale = new Vector2(1 / texture.ScaleAdjust); } protected override void LoadComplete() @@ -76,6 +58,42 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor resetTime(); } + private Texture texture = Texture.WhitePixel; + + public Texture Texture + { + get => texture; + set + { + if (texture == value) + return; + + texture = value; + Invalidate(Invalidation.DrawNode); + } + } + + private readonly Cached partSizeCache = new Cached(); + + private Vector2 partSize => partSizeCache.IsValid + ? partSizeCache.Value + : (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy); + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence)) > 0) + partSizeCache.Invalidate(); + + return base.Invalidate(invalidation, source, shallPropagate); + } + + /// + /// The amount of time to fade the cursor trail pieces. + /// + protected virtual double FadeDuration => 300; + + public override bool IsPresent => true; + protected override void Update() { base.Update(); @@ -84,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor const int fade_clock_reset_threshold = 1000000; - time = (float)(Time.Current - timeOffset) / 300f; + time = (float)((Time.Current - timeOffset) / FadeDuration); if (time > fade_clock_reset_threshold) resetTime(); } @@ -101,6 +119,16 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor timeOffset = Time.Current; } + /// + /// Whether to interpolate mouse movements and add trail pieces at intermediate points. + /// + protected virtual bool InterpolateMovements => true; + + private Vector2? lastPosition; + private readonly InputResampler resampler = new InputResampler(); + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + protected override bool OnMouseMove(MouseMoveEvent e) { Vector2 pos = e.ScreenSpaceMousePosition; @@ -116,33 +144,43 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { Trace.Assert(lastPosition.HasValue); - // ReSharper disable once PossibleInvalidOperationException - Vector2 pos1 = lastPosition.Value; - Vector2 diff = pos2 - pos1; - float distance = diff.Length; - Vector2 direction = diff / distance; - - float interval = size.X / 2 * 0.9f; - - for (float d = interval; d < distance; d += interval) + if (InterpolateMovements) { - lastPosition = pos1 + direction * d; - addPosition(lastPosition.Value); + // ReSharper disable once PossibleInvalidOperationException + Vector2 pos1 = lastPosition.Value; + Vector2 diff = pos2 - pos1; + float distance = diff.Length; + Vector2 direction = diff / distance; + + float interval = partSize.X / 2.5f; + + for (float d = interval; d < distance; d += interval) + { + lastPosition = pos1 + direction * d; + addPart(lastPosition.Value); + } + } + else + { + lastPosition = pos2; + addPart(lastPosition.Value); } } return base.OnMouseMove(e); } - private void addPosition(Vector2 pos) + private void addPart(Vector2 screenSpacePosition) { - parts[currentIndex].Position = pos; + parts[currentIndex].Position = screenSpacePosition; parts[currentIndex].Time = time; ++parts[currentIndex].InvalidationID; currentIndex = (currentIndex + 1) % max_sprites; } + protected override DrawNode CreateDrawNode() => new TrailDrawNode(this); + private struct TrailPart { public Vector2 Position; @@ -177,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor shader = Source.shader; texture = Source.texture; - size = Source.size; + size = Source.partSize; time = Source.time; for (int i = 0; i < Source.parts.Length; ++i) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 869c27dcac..41a02deaca 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private bool cursorExpand; - private Bindable cursorScale; + private Bindable cursorScale; private Bindable autoCursorScale; private readonly IBindable beatmap = new Bindable(); @@ -38,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor protected override void SkinChanged(ISkinSource skin, bool allowFallback) { - cursorExpand = skin.GetValue(s => s.CursorExpand ?? true); + cursorExpand = skin.GetConfig(OsuSkinConfiguration.CursorExpand)?.Value ?? true; } [BackgroundDependencyLoader] @@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor this.beatmap.BindTo(beatmap); this.beatmap.ValueChanged += _ => calculateScale(); - cursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); + cursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); cursorScale.ValueChanged += _ => calculateScale(); autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize); @@ -70,12 +71,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private void calculateScale() { - float scale = (float)cursorScale.Value; + float scale = cursorScale.Value; if (autoCursorScale.Value && beatmap.Value != null) { // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. - scale *= (float)(1 - 0.7 * (1 + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY); + scale *= 1f - 0.7f * (1f + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY; } scaleTarget.Scale = new Vector2(scale); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 893c7875fa..a944ff88c6 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -6,9 +6,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.UI; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.UI.Cursor { @@ -22,17 +25,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly Bindable showTrail = new Bindable(true); - private readonly CursorTrail cursorTrail; + private readonly Drawable cursorTrail; public OsuCursorContainer() { InternalChild = fadeContainer = new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - cursorTrail = new CursorTrail { Depth = 1 } - } + Child = cursorTrail = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling) }; } @@ -98,5 +98,15 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor fadeContainer.FadeTo(0.05f, 450, Easing.OutQuint); ActiveCursor.ScaleTo(0.8f, 450, Easing.OutQuint); } + + private class DefaultCursorTrail : CursorTrail + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get(@"Cursor/cursortrail"); + Scale = new Vector2(1 / Texture.ScaleAdjust); + } + } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index ea7eee8bb8..d1757de445 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -70,13 +70,7 @@ namespace osu.Game.Rulesets.Osu.UI base.Add(h); } - private void addApproachCircleProxy(Drawable d) - { - var proxy = d.CreateProxy(); - proxy.LifetimeStart = d.LifetimeStart; - proxy.LifetimeEnd = d.LifetimeEnd; - approachCircles.Add(proxy); - } + private void addApproachCircleProxy(Drawable d) => approachCircles.Add(d.CreateProxy()); public override void PostProcess() { @@ -92,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.UI { Origin = Anchor.Centre, Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition, - Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale * 1.65f) + Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale) }; judgementLayer.Add(explosion); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs new file mode 100644 index 0000000000..a59544386b --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Tests.Beatmaps; + +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) })] + [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); + + protected override Ruleset CreateRuleset() => new TaikoRuleset(); + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs index 6fd16c213b..cbbf5b0c09 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs @@ -14,13 +14,13 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Tests.Visual; using osuTK; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Tests { @@ -53,6 +53,11 @@ namespace osu.Game.Rulesets.Taiko.Tests AddStep("Strong Rim", () => addRimHit(true)); AddStep("Add bar line", () => addBarLine(false)); AddStep("Add major bar line", () => addBarLine(true)); + AddStep("Add centre w/ bar line", () => + { + addCentreHit(false); + addBarLine(true); + }); AddStep("Height test 1", () => changePlayfieldSize(1)); AddStep("Height test 2", () => changePlayfieldSize(2)); AddStep("Height test 3", () => changePlayfieldSize(3)); diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index d2a0a8fa6f..b0e0efdc68 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index fc93bccb94..32d49ea39c 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -8,11 +8,12 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; namespace osu.Game.Rulesets.Taiko.Difficulty { @@ -38,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty StarRating = skills.Single().DifficultyValue() * star_scaling_factor, Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)(hitWindows.Great / 2) / clockRate, + GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; diff --git a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs index a07012fd71..2afbbc737c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Objects; + namespace osu.Game.Rulesets.Taiko.Objects { - public class BarLine : TaikoHitObject + public class BarLine : TaikoHitObject, IBarLine { + public bool Major { get; set; } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index bf89f7e15b..1a5a797f28 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Objects; using osuTK; using osu.Game.Rulesets.Objects.Drawables; @@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// A line that scrolls alongside hit objects in the playfield and visualises control points. /// - public class DrawableBarLine : DrawableHitObject + public class DrawableBarLine : DrawableHitObject { /// /// The width of the line tracker. diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index f4407a7b54..8e16a21199 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { case ArmedState.Hit: case ArmedState.Miss: - this.Delay(HitObject.Duration).FadeOut(100).Expire(); + this.Delay(HitObject.Duration).FadeOut(100); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index cef9a53deb..25b6141a0e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (state) { case ArmedState.Hit: - this.ScaleTo(0, 100, Easing.OutQuint).Expire(); + this.ScaleTo(0, 100, Easing.OutQuint); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 0942b37f58..4b25ff0ecc 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -105,12 +105,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables validActionPressed = false; UnproxyContent(); - this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire(); break; case ArmedState.Miss: - this.FadeOut(100) - .Expire(); + this.FadeOut(100); break; case ArmedState.Hit: @@ -129,9 +127,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables .Then() .MoveToY(gravity_travel_height * 2, gravity_time * 2, Easing.In); - this.FadeOut(800) - .Expire(); - + this.FadeOut(800); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 094ad1230f..07af7fe7e0 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -208,8 +208,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { this.FadeOut(transition_duration, Easing.Out); bodyContainer.ScaleTo(1.4f, transition_duration); - - Expire(); } break; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 5424ccb4de..423f65b2d3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // Normal and clap samples are handled by the drum protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); - protected override string SampleNamespace => "Taiko"; + protected override string SampleNamespace => "taiko"; protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(); diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 3ed52f21f0..4e02c76a8b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -6,7 +6,7 @@ using System; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 39e2b45e24..c466ca7c8a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -2,7 +2,7 @@ // 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.Scoring; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs index 830e640242..d660149528 100644 --- a/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs @@ -2,7 +2,7 @@ // 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.Scoring; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index e7812841bf..f96c033dce 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -4,7 +4,7 @@ using System; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects diff --git a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs index 049fa7de5f..68212e8f12 100644 --- a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs @@ -2,7 +2,7 @@ // 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.Scoring; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 3592d73004..6f4fbd0651 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -4,7 +4,9 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Scoring; namespace osu.Game.Rulesets.Taiko.Objects { diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs deleted file mode 100644 index f232919cbf..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Taiko.Objects -{ - public class TaikoHitWindows : HitWindows - { - private static readonly IReadOnlyDictionary base_ranges = new Dictionary - { - { HitResult.Great, (100, 70, 40) }, - { HitResult.Good, (240, 160, 100) }, - { HitResult.Miss, (270, 190, 140) }, - }; - - public override bool IsHitResultAllowed(HitResult result) - { - switch (result) - { - case HitResult.Great: - case HitResult.Good: - case HitResult.Miss: - return true; - - default: - return false; - } - } - - public override void SetDifficulty(double difficulty) - { - Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); - Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); - Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs index 5203415e90..c5ebefc397 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs @@ -23,12 +23,12 @@ namespace osu.Game.Rulesets.Taiko.Replays Actions.AddRange(actions); } - public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap) + public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { - if (legacyFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim); - if (legacyFrame.MouseRight2) Actions.Add(TaikoAction.RightRim); - if (legacyFrame.MouseLeft1) Actions.Add(TaikoAction.LeftCentre); - if (legacyFrame.MouseLeft2) Actions.Add(TaikoAction.RightCentre); + if (currentFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim); + if (currentFrame.MouseRight2) Actions.Add(TaikoAction.RightRim); + if (currentFrame.MouseLeft1) Actions.Add(TaikoAction.LeftCentre); + if (currentFrame.MouseLeft2) Actions.Add(TaikoAction.RightCentre); } } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs new file mode 100644 index 0000000000..9d273392ff --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Taiko.Scoring +{ + public class TaikoHitWindows : HitWindows + { + private static readonly DifficultyRange[] taiko_ranges = + { + new DifficultyRange(HitResult.Great, 50, 35, 20), + new DifficultyRange(HitResult.Good, 120, 80, 50), + new DifficultyRange(HitResult.Miss, 135, 95, 70), + }; + + public override bool IsHitResultAllowed(HitResult result) + { + switch (result) + { + case HitResult.Great: + case HitResult.Good: + case HitResult.Miss: + return true; + } + + return false; + } + + protected override DifficultyRange[] GetRanges() => taiko_ranges; + } +} diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 68ddf2db19..75a27ff639 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -3,7 +3,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 7fdb823388..b2655f592c 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -45,6 +45,11 @@ namespace osu.Game.Rulesets.Taiko else if (mods.HasFlag(LegacyMods.DoubleTime)) yield return new TaikoModDoubleTime(); + if (mods.HasFlag(LegacyMods.Perfect)) + yield return new TaikoModPerfect(); + else if (mods.HasFlag(LegacyMods.SuddenDeath)) + yield return new TaikoModSuddenDeath(); + if (mods.HasFlag(LegacyMods.Autoplay)) yield return new TaikoModAutoplay(); @@ -66,14 +71,8 @@ namespace osu.Game.Rulesets.Taiko if (mods.HasFlag(LegacyMods.NoFail)) yield return new TaikoModNoFail(); - if (mods.HasFlag(LegacyMods.Perfect)) - yield return new TaikoModPerfect(); - if (mods.HasFlag(LegacyMods.Relax)) yield return new TaikoModRelax(); - - if (mods.HasFlag(LegacyMods.SuddenDeath)) - yield return new TaikoModSuddenDeath(); } public override IEnumerable GetModsFor(ModType type) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index b03bea578e..fc109bf6a6 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -5,19 +5,18 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Taiko.Replays; -using System.Linq; using osu.Framework.Input; using osu.Game.Configuration; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Taiko.UI @@ -38,49 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - loadBarLines(); - } - - private void loadBarLines() - { - TaikoHitObject lastObject = Beatmap.HitObjects[Beatmap.HitObjects.Count - 1]; - double lastHitTime = 1 + ((lastObject as IHasEndTime)?.EndTime ?? lastObject.StartTime); - - var timingPoints = Beatmap.ControlPointInfo.TimingPoints.ToList(); - - if (timingPoints.Count == 0) - return; - - int currentIndex = 0; - int currentBeat = 0; - double time = timingPoints[currentIndex].Time; - - while (time <= lastHitTime) - { - int nextIndex = currentIndex + 1; - - if (nextIndex < timingPoints.Count && time > timingPoints[nextIndex].Time) - { - currentIndex = nextIndex; - time = timingPoints[currentIndex].Time; - currentBeat = 0; - } - - var currentPoint = timingPoints[currentIndex]; - - var barLine = new BarLine - { - StartTime = time, - }; - - barLine.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty); - - bool isMajor = currentBeat % (int)currentPoint.TimeSignature == 0; - Playfield.Add(isMajor ? new DrawableBarLineMajor(barLine) : new DrawableBarLine(barLine)); - - time += currentPoint.BeatLength * (int)currentPoint.TimeSignature; - currentBeat++; - } + new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar))); } public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this); diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 9766da9a24..5234ae1f69 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -132,10 +132,10 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load(TextureStore textures, OsuColour colours) { - rim.Texture = textures.Get(@"Gameplay/Taiko/taiko-drum-outer"); - rimHit.Texture = textures.Get(@"Gameplay/Taiko/taiko-drum-outer-hit"); - centre.Texture = textures.Get(@"Gameplay/Taiko/taiko-drum-inner"); - centreHit.Texture = textures.Get(@"Gameplay/Taiko/taiko-drum-inner-hit"); + rim.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer"); + rimHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer-hit"); + centre.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner"); + centreHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner-hit"); rimHit.Colour = colours.Blue; centreHit.Colour = colours.Pink; diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index a725c58462..4859abbb8e 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -9,8 +9,8 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO.Serialization; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Resources; using osuTK; diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index ad0ed00989..385ab4064d 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -15,7 +15,10 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.IO; using osu.Game.Tests.Resources; +using SharpCompress.Archives; using SharpCompress.Archives.Zip; +using SharpCompress.Common; +using SharpCompress.Writers.Zip; namespace osu.Game.Tests.Beatmaps.IO { @@ -87,6 +90,48 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public async Task TestImportCorruptThenImport() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport")) + { + try + { + var osu = loadOsu(host); + + var imported = await LoadOszIntoOsu(osu); + + var firstFile = imported.Files.First(); + + var files = osu.Dependencies.Get(); + + long originalLength; + using (var stream = files.Storage.GetStream(firstFile.FileInfo.StoragePath)) + originalLength = stream.Length; + + using (var stream = files.Storage.GetStream(firstFile.FileInfo.StoragePath, FileAccess.Write, FileMode.Create)) + stream.WriteByte(0); + + var importedSecondTime = await LoadOszIntoOsu(osu); + + using (var stream = files.Storage.GetStream(firstFile.FileInfo.StoragePath)) + Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import"); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + + checkBeatmapSetCount(osu, 1); + checkSingleReferencedFileCount(osu, 18); + } + finally + { + host.Exit(); + } + } + } + [Test] public async Task TestRollbackOnFailure() { @@ -135,7 +180,7 @@ namespace osu.Game.Tests.Beatmaps.IO using (var zip = ZipArchive.Open(brokenOsz)) { zip.AddEntry("broken.osu", brokenOsu, false); - zip.SaveTo(outStream, SharpCompress.Common.CompressionType.Deflate); + zip.SaveTo(outStream, CompressionType.Deflate); } // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu. @@ -366,6 +411,51 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public async Task TestImportNestedStructure() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportNestedStructure")) + { + try + { + var osu = loadOsu(host); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + string subfolder = Path.Combine(extractedFolder, "subfolder"); + + Directory.CreateDirectory(subfolder); + + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(subfolder); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var imported = await osu.Dependencies.Get().Import(temp); + + ensureLoaded(osu); + + Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder"); + } + finally + { + Directory.Delete(extractedFolder, true); + } + } + finally + { + host.Exit(); + } + } + } + public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null) { var temp = path ?? TestResources.GetTestBeatmapForImport(); diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 0d6ed67767..9b4a90e9a9 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -119,6 +119,53 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(11, result.Links[0].Length); } + [Test] + public void TestOldFormatLinkWithBalancedBrackets() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a (tricky (one))[https://osu.ppy.sh]!" }); + + Assert.AreEqual("This is a tricky (one)!", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(12, result.Links[0].Length); + } + + [Test] + public void TestOldFormatLinkWithEscapedBrackets() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is (another loose bracket \\))[https://osu.ppy.sh]." }); + + Assert.AreEqual("This is another loose bracket ).", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(8, result.Links[0].Index); + Assert.AreEqual(23, result.Links[0].Length); + } + + [Test] + public void TestOldFormatWithBackslashes() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This link (should end with a backslash \\)[https://osu.ppy.sh]." }); + Assert.AreEqual("This link should end with a backslash \\.", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(29, result.Links[0].Length); + } + + [Test] + public void TestOldFormatLinkWithEscapedAndBalancedBrackets() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a (\\)super\\(\\( tricky (one))[https://osu.ppy.sh]!" }); + + Assert.AreEqual("This is a )super(( tricky (one)!", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(21, result.Links[0].Length); + } + [Test] public void TestNewFormatLink() { @@ -131,6 +178,42 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(11, result.Links[0].Length); } + [Test] + public void TestNewFormatLinkWithEscapedBrackets() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [https://osu.ppy.sh nasty link with escaped brackets: \\] and \\[]" }); + + Assert.AreEqual("This is a nasty link with escaped brackets: ] and [", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(41, result.Links[0].Length); + } + + [Test] + public void TestNewFormatLinkWithBackslashesInside() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [https://osu.ppy.sh link \\ with \\ backslashes \\]" }); + + Assert.AreEqual("This is a link \\ with \\ backslashes \\", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(27, result.Links[0].Length); + } + + [Test] + public void TestNewFormatLinkWithEscapedAndBalancedBrackets() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [https://osu.ppy.sh [link [with \\] too many brackets \\[ ]]]" }); + + Assert.AreEqual("This is a [link [with ] too many brackets [ ]]", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(36, result.Links[0].Length); + } + [Test] public void TestMarkdownFormatLink() { @@ -143,6 +226,53 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(11, result.Links[0].Length); } + [Test] + public void TestMarkdownFormatLinkWithBalancedBrackets() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [tricky [one]](https://osu.ppy.sh)!" }); + + Assert.AreEqual("This is a tricky [one]!", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(12, result.Links[0].Length); + } + + [Test] + public void TestMarkdownFormatLinkWithEscapedBrackets() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is [another loose bracket \\]](https://osu.ppy.sh)." }); + + Assert.AreEqual("This is another loose bracket ].", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(8, result.Links[0].Index); + Assert.AreEqual(23, result.Links[0].Length); + } + + [Test] + public void TestMarkdownFormatWithBackslashes() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This link [should end with a backslash \\](https://osu.ppy.sh)." }); + Assert.AreEqual("This link should end with a backslash \\.", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(29, result.Links[0].Length); + } + + [Test] + public void TestMarkdownFormatLinkWithEscapedAndBalancedBrackets() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [\\]super\\[\\[ tricky [one]](https://osu.ppy.sh)!" }); + + Assert.AreEqual("This is a ]super[[ tricky [one]!", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(21, result.Links[0].Length); + } + [Test] public void TestChannelLink() { diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs new file mode 100644 index 0000000000..30686cb947 --- /dev/null +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -0,0 +1,201 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Carousel; + +namespace osu.Game.Tests.NonVisual.Filtering +{ + [TestFixture] + public class FilterMatchingTest + { + private BeatmapInfo getExampleBeatmap() => new BeatmapInfo + { + Ruleset = new RulesetInfo { ID = 5 }, + StarDifficulty = 4.0d, + BaseDifficulty = new BeatmapDifficulty + { + ApproachRate = 5.0f, + DrainRate = 3.0f, + CircleSize = 2.0f, + }, + Metadata = new BeatmapMetadata + { + Artist = "The Artist", + ArtistUnicode = "check unicode too", + Title = "Title goes here", + TitleUnicode = "Title goes here", + AuthorString = "The Author", + Source = "unit tests", + Tags = "look for tags too", + }, + Version = "version as well", + Length = 2500, + BPM = 160, + BeatDivisor = 12, + Status = BeatmapSetOnlineStatus.Loved + }; + + [Test] + public void TestCriteriaMatchingNoRuleset() + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria(); + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.IsFalse(carouselItem.Filtered.Value); + } + + [Test] + public void TestCriteriaMatchingSpecificRuleset() + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ID = 6 } + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.IsTrue(carouselItem.Filtered.Value); + } + + [Test] + public void TestCriteriaMatchingConvertedBeatmaps() + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ID = 6 }, + AllowConvertedBeatmaps = true + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.IsFalse(carouselItem.Filtered.Value); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void TestCriteriaMatchingRangeMin(bool inclusive) + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ID = 6 }, + AllowConvertedBeatmaps = true, + ApproachRate = new FilterCriteria.OptionalRange + { + IsLowerInclusive = inclusive, + Min = 5.0f + } + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(!inclusive, carouselItem.Filtered.Value); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void TestCriteriaMatchingRangeMax(bool inclusive) + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ID = 6 }, + AllowConvertedBeatmaps = true, + BPM = new FilterCriteria.OptionalRange + { + IsUpperInclusive = inclusive, + Max = 160d + } + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(!inclusive, carouselItem.Filtered.Value); + } + + [Test] + [TestCase("artist", false)] + [TestCase("artist title author", false)] + [TestCase("an artist", true)] + [TestCase("tags too", false)] + [TestCase("version", false)] + [TestCase("an auteur", true)] + public void TestCriteriaMatchingTerms(string terms, bool filtered) + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Ruleset = new RulesetInfo { ID = 6 }, + AllowConvertedBeatmaps = true, + SearchText = terms + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(filtered, carouselItem.Filtered.Value); + } + + [Test] + [TestCase("", false)] + [TestCase("The", false)] + [TestCase("THE", false)] + [TestCase("author", false)] + [TestCase("the author", false)] + [TestCase("the author AND then something else", true)] + [TestCase("unknown", true)] + public void TestCriteriaMatchingCreator(string creatorName, bool filtered) + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = creatorName } + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(filtered, carouselItem.Filtered.Value); + } + + [Test] + [TestCase("", false)] + [TestCase("The", false)] + [TestCase("THE", false)] + [TestCase("artist", false)] + [TestCase("the artist", false)] + [TestCase("the artist AND then something else", true)] + [TestCase("unicode too", false)] + [TestCase("unknown", true)] + public void TestCriteriaMatchingArtist(string artistName, bool filtered) + { + var exampleBeatmapInfo = getExampleBeatmap(); + var criteria = new FilterCriteria + { + Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName } + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(filtered, carouselItem.Filtered.Value); + } + + [Test] + [TestCase("", false)] + [TestCase("artist", false)] + [TestCase("unknown", true)] + public void TestCriteriaMatchingArtistWithNullUnicodeName(string artistName, bool filtered) + { + var exampleBeatmapInfo = getExampleBeatmap(); + exampleBeatmapInfo.Metadata.ArtistUnicode = null; + + var criteria = new FilterCriteria + { + Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName } + }; + var carouselItem = new CarouselBeatmap(exampleBeatmapInfo); + carouselItem.Filter(criteria); + Assert.AreEqual(filtered, carouselItem.Filtered.Value); + } + } +} diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs new file mode 100644 index 0000000000..9869ddde41 --- /dev/null +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -0,0 +1,184 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; + +namespace osu.Game.Tests.NonVisual.Filtering +{ + [TestFixture] + public class FilterQueryParserTest + { + [Test] + public void TestApplyQueriesBareWords() + { + const string query = "looking for a beatmap"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("looking for a beatmap", filterCriteria.SearchText); + Assert.AreEqual(4, filterCriteria.SearchTerms.Length); + } + + /* + * The following tests have been written a bit strangely (they don't check exact + * bound equality with what the filter says). + * This is to account for floating-point arithmetic issues. + * For example, specifying a bpm<140 filter would previously match beatmaps with BPM + * of 139.99999, which would be displayed in the UI as 140. + * Due to this the tests check the last tick inside the range and the first tick + * outside of the range. + */ + + [Test] + public void TestApplyStarQueries() + { + const string query = "stars<4 easy"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("easy", filterCriteria.SearchText.Trim()); + Assert.AreEqual(1, filterCriteria.SearchTerms.Length); + Assert.IsNotNull(filterCriteria.StarDifficulty.Max); + Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d); + Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d); + Assert.IsNull(filterCriteria.StarDifficulty.Min); + } + + [Test] + public void TestApplyApproachRateQueries() + { + const string query = "ar>=9 difficult"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("difficult", filterCriteria.SearchText.Trim()); + Assert.AreEqual(1, filterCriteria.SearchTerms.Length); + Assert.IsNotNull(filterCriteria.ApproachRate.Min); + Assert.Greater(filterCriteria.ApproachRate.Min, 8.9f); + Assert.Less(filterCriteria.ApproachRate.Min, 9.0f); + Assert.IsNull(filterCriteria.ApproachRate.Max); + } + + [Test] + public void TestApplyDrainRateQueries() + { + const string query = "dr>2 quite specific dr<:6"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim()); + Assert.AreEqual(2, filterCriteria.SearchTerms.Length); + Assert.Greater(filterCriteria.DrainRate.Min, 2.0f); + Assert.Less(filterCriteria.DrainRate.Min, 2.1f); + Assert.Greater(filterCriteria.DrainRate.Max, 6.0f); + Assert.Less(filterCriteria.DrainRate.Min, 6.1f); + } + + [Test] + public void TestApplyBPMQueries() + { + const string query = "bpm>:200 gotta go fast"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("gotta go fast", filterCriteria.SearchText.Trim()); + Assert.AreEqual(3, filterCriteria.SearchTerms.Length); + Assert.IsNotNull(filterCriteria.BPM.Min); + Assert.Greater(filterCriteria.BPM.Min, 199.99d); + Assert.Less(filterCriteria.BPM.Min, 200.00d); + Assert.IsNull(filterCriteria.BPM.Max); + } + + private static object[] lengthQueryExamples = + { + new object[] { "6ms", TimeSpan.FromMilliseconds(6), TimeSpan.FromMilliseconds(1) }, + new object[] { "23s", TimeSpan.FromSeconds(23), TimeSpan.FromSeconds(1) }, + new object[] { "9m", TimeSpan.FromMinutes(9), TimeSpan.FromMinutes(1) }, + new object[] { "0.25h", TimeSpan.FromHours(0.25), TimeSpan.FromHours(1) }, + new object[] { "70", TimeSpan.FromSeconds(70), TimeSpan.FromSeconds(1) }, + }; + + [Test] + [TestCaseSource(nameof(lengthQueryExamples))] + public void TestApplyLengthQueries(string lengthQuery, TimeSpan expectedLength, TimeSpan scale) + { + string query = $"length={lengthQuery} time"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("time", filterCriteria.SearchText.Trim()); + Assert.AreEqual(1, filterCriteria.SearchTerms.Length); + Assert.AreEqual(expectedLength.TotalMilliseconds - scale.TotalMilliseconds / 2.0, filterCriteria.Length.Min); + Assert.AreEqual(expectedLength.TotalMilliseconds + scale.TotalMilliseconds / 2.0, filterCriteria.Length.Max); + } + + [Test] + public void TestApplyDivisorQueries() + { + const string query = "that's a time signature alright! divisor:12"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("that's a time signature alright!", filterCriteria.SearchText.Trim()); + Assert.AreEqual(5, filterCriteria.SearchTerms.Length); + Assert.AreEqual(12, filterCriteria.BeatDivisor.Min); + Assert.IsTrue(filterCriteria.BeatDivisor.IsLowerInclusive); + Assert.AreEqual(12, filterCriteria.BeatDivisor.Max); + Assert.IsTrue(filterCriteria.BeatDivisor.IsUpperInclusive); + } + + [Test] + public void TestApplyStatusQueries() + { + const string query = "I want the pp status=ranked"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim()); + Assert.AreEqual(4, filterCriteria.SearchTerms.Length); + Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min); + Assert.IsTrue(filterCriteria.OnlineStatus.IsLowerInclusive); + Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max); + Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive); + } + + [Test] + public void TestApplyCreatorQueries() + { + const string query = "beatmap specifically by creator=my_fav"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("beatmap specifically by", filterCriteria.SearchText.Trim()); + Assert.AreEqual(3, filterCriteria.SearchTerms.Length); + Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm); + } + + [Test] + public void TestApplyArtistQueries() + { + const string query = "find me songs by artist=singer please"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim()); + Assert.AreEqual(5, filterCriteria.SearchTerms.Length); + Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm); + } + + [Test] + public void TestApplyArtistQueriesWithSpaces() + { + const string query = "really like artist=\"name with space\" yes"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim()); + Assert.AreEqual(3, filterCriteria.SearchTerms.Length); + Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm); + } + + [Test] + public void TestApplyArtistQueriesOneDoubleQuote() + { + const string query = "weird artist=double\"quote"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("weird", filterCriteria.SearchText.Trim()); + Assert.AreEqual(1, filterCriteria.SearchTerms.Length); + Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm); + } + } +} diff --git a/osu.Game.Tests/Resources/skin.ini b/osu.Game.Tests/Resources/skin.ini index 0e5737b4ea..7f7f0b32a6 100644 --- a/osu.Game.Tests/Resources/skin.ini +++ b/osu.Game.Tests/Resources/skin.ini @@ -1,5 +1,6 @@ [General] Name: test skin +TestLookup: TestValue [Colours] Combo1 : 142,199,255 diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 4babb07213..89b5db9e1b 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -117,17 +117,57 @@ namespace osu.Game.Tests.Scores.IO } } + [Test] + public async Task TestImportWithDeletedBeatmapSet() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDeletedBeatmapSet")) + { + try + { + var osu = await loadOsu(host); + + var toImport = new ScoreInfo + { + Hash = Guid.NewGuid().ToString(), + Statistics = new Dictionary + { + { HitResult.Perfect, 100 }, + { HitResult.Miss, 50 } + } + }; + + var imported = await loadIntoOsu(osu, toImport); + + var beatmapManager = osu.Dependencies.Get(); + var scoreManager = osu.Dependencies.Get(); + + beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID))); + Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); + + var secondImport = await loadIntoOsu(osu, imported); + Assert.That(secondImport, Is.Null); + } + finally + { + host.Exit(); + } + } + } + private async Task loadIntoOsu(OsuGameBase osu, ScoreInfo score) { var beatmapManager = osu.Dependencies.Get(); - score.Beatmap = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - score.Ruleset = new OsuRuleset().RulesetInfo; + if (score.Beatmap == null) + score.Beatmap = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + if (score.Ruleset == null) + score.Ruleset = new OsuRuleset().RulesetInfo; var scoreManager = osu.Dependencies.Get(); await scoreManager.Import(score); - return scoreManager.GetAllUsableScores().First(); + return scoreManager.GetAllUsableScores().FirstOrDefault(); } private async Task loadOsu(GameHost host) diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs index 24ef9e4535..8bd846518b 100644 --- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs @@ -41,5 +41,20 @@ namespace osu.Game.Tests.Skins Assert.AreEqual(expectedColors[i], comboColors[i]); } } + + [Test] + public void TestDecodeGeneral() + { + var decoder = new LegacySkinDecoder(); + + using (var resStream = TestResources.OpenResource("skin.ini")) + using (var stream = new StreamReader(resStream)) + { + var config = decoder.Decode(stream); + + Assert.AreEqual("test skin", config.SkinInfo.Name); + Assert.AreEqual("TestValue", config.ConfigDictionary["TestLookup"]); + } + } } } diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs new file mode 100644 index 0000000000..578030748b --- /dev/null +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -0,0 +1,158 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; +using osu.Game.Audio; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Tests.Skins +{ + [TestFixture] + [HeadlessTest] + public class TestSceneSkinConfigurationLookup : OsuTestScene + { + private LegacySkin source1; + private LegacySkin source2; + private SkinRequester requester; + + [SetUp] + public void SetUp() => Schedule(() => + { + Add(new SkinProvidingContainer(source1 = new SkinSource()) + .WithChild(new SkinProvidingContainer(source2 = new SkinSource()) + .WithChild(requester = new SkinRequester()))); + }); + + [Test] + public void TestBasicLookup() + { + AddStep("Add config values", () => + { + source1.Configuration.ConfigDictionary["Lookup"] = "source1"; + source2.Configuration.ConfigDictionary["Lookup"] = "source2"; + }); + + AddAssert("Check lookup finds source2", () => requester.GetConfig("Lookup")?.Value == "source2"); + } + + [Test] + public void TestFloatLookup() + { + AddStep("Add config values", () => source1.Configuration.ConfigDictionary["FloatTest"] = "1.1"); + AddAssert("Check float parse lookup", () => requester.GetConfig("FloatTest")?.Value == 1.1f); + } + + [Test] + public void TestBoolLookup() + { + AddStep("Add config values", () => source1.Configuration.ConfigDictionary["BoolTest"] = "1"); + AddAssert("Check bool parse lookup", () => requester.GetConfig("BoolTest")?.Value == true); + } + + [Test] + public void TestEnumLookup() + { + AddStep("Add config values", () => source1.Configuration.ConfigDictionary["Test"] = "Test2"); + AddAssert("Check enum parse lookup", () => requester.GetConfig(LookupType.Test)?.Value == ValueType.Test2); + } + + [Test] + public void TestLookupFailure() + { + AddAssert("Check lookup failure", () => requester.GetConfig("Lookup") == null); + } + + [Test] + public void TestLookupNull() + { + AddStep("Add config values", () => source1.Configuration.ConfigDictionary["Lookup"] = null); + + AddAssert("Check lookup null", () => + { + var bindable = requester.GetConfig("Lookup"); + return bindable != null && bindable.Value == null; + }); + } + + [Test] + public void TestColourLookup() + { + AddStep("Add config colour", () => source1.Configuration.CustomColours["Lookup"] = Color4.Red); + AddAssert("Check colour lookup", () => requester.GetConfig(new SkinCustomColourLookup("Lookup"))?.Value == Color4.Red); + } + + [Test] + public void TestGlobalLookup() + { + AddAssert("Check combo colours", () => requester.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value?.Count > 0); + } + + [Test] + public void TestWrongColourType() + { + AddStep("Add config colour", () => source1.Configuration.CustomColours["Lookup"] = Color4.Red); + + AddAssert("perform incorrect lookup", () => + { + try + { + requester.GetConfig(new SkinCustomColourLookup("Lookup")); + return false; + } + catch + { + return true; + } + }); + } + + public enum LookupType + { + Test + } + + public enum ValueType + { + Test1, + Test2, + Test3 + } + + public class SkinSource : LegacySkin + { + public SkinSource() + : base(new SkinInfo(), null, null, string.Empty) + { + } + } + + public class SkinRequester : Drawable, ISkin + { + private ISkinSource skin; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + this.skin = skin; + } + + public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component); + + public Texture GetTexture(string componentName) => skin.GetTexture(componentName); + + public SampleChannel GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo); + + public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup); + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 452ac859de..f94071a7a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; @@ -12,6 +13,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Description("Player instantiated with an autoplay mod.")] public class TestSceneAutoplay : AllPlayersTestScene { + private ClockBackedTestWorkingBeatmap.TrackVirtualManual track; + protected override Player CreatePlayer(Ruleset ruleset) { Mods.Value = Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); @@ -21,7 +24,18 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("score above zero", () => ((ScoreAccessiblePlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0)); + AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); + AddStep("rewind", () => track.Seek(-10000)); + AddUntilStep("key counter reset", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) + { + var working = base.CreateWorkingBeatmap(beatmap); + + track = (ClockBackedTestWorkingBeatmap.TrackVirtualManual)working.Track; + + return working; } private class ScoreAccessiblePlayer : TestPlayer @@ -29,6 +43,8 @@ namespace osu.Game.Tests.Visual.Gameplay public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new HUDOverlay HUDOverlay => base.HUDOverlay; + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + public ScoreAccessiblePlayer() : base(false, false) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs index e9c15dab9b..a934d22b5d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs @@ -3,18 +3,18 @@ using NUnit.Framework; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Catch.Objects; using System; using System.Collections.Generic; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; using osu.Framework.MathUtils; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Catch.Scoring; +using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Screens.Play.HUD.HitErrorMeters; namespace osu.Game.Tests.Visual.Gameplay @@ -36,8 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("New random judgement", () => newJudgement(), 40); - AddRepeatStep("New max negative", () => newJudgement(-hitWindows.HalfWindowFor(HitResult.Meh)), 20); - AddRepeatStep("New max positive", () => newJudgement(hitWindows.HalfWindowFor(HitResult.Meh)), 20); + AddRepeatStep("New max negative", () => newJudgement(-hitWindows.WindowFor(HitResult.Meh)), 20); + AddRepeatStep("New max positive", () => newJudgement(hitWindows.WindowFor(HitResult.Meh)), 20); AddStep("New fixed judgement (50ms)", () => newJudgement(50)); } @@ -85,9 +85,9 @@ namespace osu.Game.Tests.Visual.Gameplay AutoSizeAxes = Axes.Both, Children = new[] { - new SpriteText { Text = $@"Great: {hitWindows?.Great}" }, - new SpriteText { Text = $@"Good: {hitWindows?.Good}" }, - new SpriteText { Text = $@"Meh: {hitWindows?.Meh}" }, + new SpriteText { Text = $@"Great: {hitWindows?.WindowFor(HitResult.Great)}" }, + new SpriteText { Text = $@"Good: {hitWindows?.WindowFor(HitResult.Good)}" }, + new SpriteText { Text = $@"Meh: {hitWindows?.WindowFor(HitResult.Meh)}" }, } }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs new file mode 100644 index 0000000000..cca6301b02 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneFailJudgement : AllPlayersTestScene + { + protected override Player CreatePlayer(Ruleset ruleset) + { + Mods.Value = Array.Empty(); + return new FailPlayer(); + } + + protected override void AddCheckSteps() + { + AddUntilStep("wait for fail", () => Player.HasFailed); + AddUntilStep("wait for multiple judged objects", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.Count(h => h.AllJudged) > 1); + AddAssert("total judgements == 1", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits == 1); + } + + private class FailPlayer : TestPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public FailPlayer() + : base(false, false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + ScoreProcessor.FailConditions += (_, __) => true; + } + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs index 4727140d99..c1635ffc83 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs @@ -3,12 +3,13 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Screens.Play; using osuTK; @@ -29,57 +30,118 @@ namespace osu.Game.Tests.Visual.Gameplay [BackgroundDependencyLoader] private void load(OsuGameBase game) { - Child = globalActionContainer = new GlobalActionContainer(game) - { - Children = new Drawable[] - { - pauseOverlay = new PauseOverlay - { - OnResume = () => Logger.Log(@"Resume"), - OnRetry = () => Logger.Log(@"Retry"), - OnQuit = () => Logger.Log(@"Quit"), - }, - failOverlay = new FailOverlay + Child = globalActionContainer = new GlobalActionContainer(game); + } - { - OnRetry = () => Logger.Log(@"Retry"), - OnQuit = () => Logger.Log(@"Quit"), - } + [SetUp] + public void SetUp() => Schedule(() => + { + globalActionContainer.Children = new Drawable[] + { + pauseOverlay = new PauseOverlay + { + OnResume = () => Logger.Log(@"Resume"), + OnRetry = () => Logger.Log(@"Retry"), + OnQuit = () => Logger.Log(@"Quit"), + }, + failOverlay = new FailOverlay + + { + OnRetry = () => Logger.Log(@"Retry"), + OnQuit = () => Logger.Log(@"Quit"), } }; + InputManager.MoveMouseTo(Vector2.Zero); + }); + + [Test] + public void TestAdjustRetryCount() + { + showOverlay(); + var retryCount = 0; - AddStep("Add retry", () => + AddRepeatStep("Add retry", () => { retryCount++; pauseOverlay.Retries = failOverlay.Retries = retryCount; - }); + }, 10); + } - AddToggleStep("Toggle pause overlay", t => pauseOverlay.ToggleVisibility()); - AddToggleStep("Toggle fail overlay", t => failOverlay.ToggleVisibility()); + /// + /// Tests that pressing enter after an overlay shows doesn't trigger an event because a selection hasn't occurred. + /// + [Test] + public void TestEnterWithoutSelection() + { + showOverlay(); - testHideResets(); + AddStep("Press select", () => press(GlobalAction.Select)); + AddAssert("Overlay still open", () => pauseOverlay.State.Value == Visibility.Visible); + } - testEnterWithoutSelection(); - testKeyUpFromInitial(); - testKeyDownFromInitial(); - testKeyUpWrapping(); - testKeyDownWrapping(); + /// + /// Tests that pressing the up arrow from the initial state selects the last button. + /// + [Test] + public void TestKeyUpFromInitial() + { + showOverlay(); - testMouseSelectionAfterKeySelection(); - testKeySelectionAfterMouseSelection(); + AddStep("Up arrow", () => press(Key.Up)); + AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected.Value); + } - testMouseDeselectionResets(); + /// + /// Tests that pressing the down arrow from the initial state selects the first button. + /// + [Test] + public void TestKeyDownFromInitial() + { + showOverlay(); - testClickSelection(); - testEnterKeySelection(); + AddStep("Down arrow", () => press(Key.Down)); + AddAssert("First button selected", () => getButton(0).Selected.Value); + } + + /// + /// Tests that pressing the up arrow repeatedly causes the selected button to wrap correctly. + /// + [Test] + public void TestKeyUpWrapping() + { + AddStep("Show overlay", () => failOverlay.Show()); + + AddStep("Up arrow", () => press(Key.Up)); + AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); + AddStep("Up arrow", () => press(Key.Up)); + AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); + AddStep("Up arrow", () => press(Key.Up)); + AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); + } + + /// + /// Tests that pressing the down arrow repeatedly causes the selected button to wrap correctly. + /// + [Test] + public void TestKeyDownWrapping() + { + AddStep("Show overlay", () => failOverlay.Show()); + + AddStep("Down arrow", () => press(Key.Down)); + AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); + AddStep("Down arrow", () => press(Key.Down)); + AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); + AddStep("Down arrow", () => press(Key.Down)); + AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); } /// /// Test that hiding the overlay after hovering a button will reset the overlay to the initial state with no buttons selected. /// - private void testHideResets() + [Test] + public void TestHideResets() { AddStep("Show overlay", () => failOverlay.Show()); @@ -90,141 +152,78 @@ namespace osu.Game.Tests.Visual.Gameplay } /// - /// Tests that pressing enter after an overlay shows doesn't trigger an event because a selection hasn't occurred. + /// Tests that entering menu with cursor initially on button doesn't selects it immediately. + /// This is to allow for stable keyboard navigation. /// - private void testEnterWithoutSelection() + [Test] + public void TestInitialButtonHover() { - AddStep("Show overlay", () => pauseOverlay.Show()); + showOverlay(); - AddStep("Press select", () => press(GlobalAction.Select)); - AddAssert("Overlay still open", () => pauseOverlay.State.Value == Visibility.Visible); + AddStep("Hover first button", () => InputManager.MoveMouseTo(getButton(0))); AddStep("Hide overlay", () => pauseOverlay.Hide()); - } + showOverlay(); - /// - /// Tests that pressing the up arrow from the initial state selects the last button. - /// - private void testKeyUpFromInitial() - { - AddStep("Show overlay", () => pauseOverlay.Show()); + AddAssert("First button not selected", () => !getButton(0).Selected.Value); - AddStep("Up arrow", () => press(Key.Up)); - AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected.Value); + AddStep("Move slightly", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(1))); - AddStep("Hide overlay", () => pauseOverlay.Hide()); - } - - /// - /// Tests that pressing the down arrow from the initial state selects the first button. - /// - private void testKeyDownFromInitial() - { - AddStep("Show overlay", () => pauseOverlay.Show()); - - AddStep("Down arrow", () => press(Key.Down)); - AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected.Value); - - AddStep("Hide overlay", () => pauseOverlay.Hide()); - } - - /// - /// Tests that pressing the up arrow repeatedly causes the selected button to wrap correctly. - /// - private void testKeyUpWrapping() - { - AddStep("Show overlay", () => failOverlay.Show()); - - AddStep("Up arrow", () => press(Key.Up)); - AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); - AddStep("Up arrow", () => press(Key.Up)); - AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); - AddStep("Up arrow", () => press(Key.Up)); - AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); - - AddStep("Hide overlay", () => failOverlay.Hide()); - } - - /// - /// Tests that pressing the down arrow repeatedly causes the selected button to wrap correctly. - /// - private void testKeyDownWrapping() - { - AddStep("Show overlay", () => failOverlay.Show()); - - AddStep("Down arrow", () => press(Key.Down)); - AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); - AddStep("Down arrow", () => press(Key.Down)); - AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); - AddStep("Down arrow", () => press(Key.Down)); - AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); - - AddStep("Hide overlay", () => failOverlay.Hide()); + AddAssert("First button selected", () => getButton(0).Selected.Value); } /// /// Tests that hovering a button that was previously selected with the keyboard correctly selects the new button and deselects the previous button. /// - private void testMouseSelectionAfterKeySelection() + [Test] + public void TestMouseSelectionAfterKeySelection() { - AddStep("Show overlay", () => pauseOverlay.Show()); - - var secondButton = pauseOverlay.Buttons.Skip(1).First(); + showOverlay(); AddStep("Down arrow", () => press(Key.Down)); - AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton)); - AddAssert("First button not selected", () => !pauseOverlay.Buttons.First().Selected.Value); - AddAssert("Second button selected", () => secondButton.Selected.Value); - - AddStep("Hide overlay", () => pauseOverlay.Hide()); + AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); + AddAssert("First button not selected", () => !getButton(0).Selected.Value); + AddAssert("Second button selected", () => getButton(1).Selected.Value); } /// /// Tests that pressing a key after selecting a button with a hover event correctly selects a new button and deselects the previous button. /// - private void testKeySelectionAfterMouseSelection() + [Test] + public void TestKeySelectionAfterMouseSelection() { AddStep("Show overlay", () => { pauseOverlay.Show(); - InputManager.MoveMouseTo(Vector2.Zero); }); - var secondButton = pauseOverlay.Buttons.Skip(1).First(); - - AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton)); + AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); AddStep("Up arrow", () => press(Key.Up)); - AddAssert("Second button not selected", () => !secondButton.Selected.Value); - AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected.Value); - - AddStep("Hide overlay", () => pauseOverlay.Hide()); + AddAssert("Second button not selected", () => !getButton(1).Selected.Value); + AddAssert("First button selected", () => getButton(0).Selected.Value); } /// /// Tests that deselecting with the mouse by losing hover will reset the overlay to the initial state. /// - private void testMouseDeselectionResets() + [Test] + public void TestMouseDeselectionResets() { - AddStep("Show overlay", () => pauseOverlay.Show()); + showOverlay(); - var secondButton = pauseOverlay.Buttons.Skip(1).First(); - - AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton)); + AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero)); AddStep("Down arrow", () => press(Key.Down)); - AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected.Value); // Initial state condition - - AddStep("Hide overlay", () => pauseOverlay.Hide()); + AddAssert("First button selected", () => getButton(0).Selected.Value); // Initial state condition } /// /// Tests that clicking on a button correctly causes a click event for that button. /// - private void testClickSelection() + [Test] + public void TestClickSelection() { - AddStep("Show overlay", () => pauseOverlay.Show()); - - var retryButton = pauseOverlay.Buttons.Skip(1).First(); + showOverlay(); bool triggered = false; AddStep("Click retry button", () => @@ -232,7 +231,7 @@ namespace osu.Game.Tests.Visual.Gameplay var lastAction = pauseOverlay.OnRetry; pauseOverlay.OnRetry = () => triggered = true; - retryButton.Click(); + getButton(1).Click(); pauseOverlay.OnRetry = lastAction; }); @@ -243,9 +242,10 @@ namespace osu.Game.Tests.Visual.Gameplay /// /// Tests that pressing the enter key with a button selected correctly causes a click event for that button. /// - private void testEnterKeySelection() + [Test] + public void TestEnterKeySelection() { - AddStep("Show overlay", () => pauseOverlay.Show()); + showOverlay(); AddStep("Select second button", () => { @@ -275,6 +275,10 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden); } + private void showOverlay() => AddStep("Show overlay", () => pauseOverlay.Show()); + + private DialogButton getButton(int index) => pauseOverlay.Buttons.Skip(index).First(); + private void press(Key key) { InputManager.PressKey(key); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 237fee1594..ffc025a942 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osuTK; @@ -47,9 +48,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for track to start running", () => track.IsRunning); addSeekStep(3000); AddAssert("all judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); + AddUntilStep("key counter counted keys", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7)); AddStep("clear results", () => player.AppliedResults.Clear()); addSeekStep(0); AddAssert("none judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); + AddUntilStep("key counters reset", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddAssert("no results triggered", () => player.AppliedResults.Count == 0); } @@ -90,6 +93,10 @@ namespace osu.Game.Tests.Visual.Gameplay { public readonly List AppliedResults = new List(); + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public new HUDOverlay HUDOverlay => base.HUDOverlay; + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 18088a9a5b..ad747e88e1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -7,7 +7,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.MathUtils; -using osu.Framework.Timing; using osu.Game.Screens.Play; using osuTK.Input; @@ -25,14 +24,15 @@ namespace osu.Game.Tests.Visual.Gameplay public TestSceneKeyCounter() { - KeyCounterKeyboard rewindTestKeyCounterKeyboard; + KeyCounterKeyboard testCounter; + KeyCounterDisplay kc = new KeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, Children = new KeyCounter[] { - rewindTestKeyCounterKeyboard = new KeyCounterKeyboard(Key.X), + testCounter = new KeyCounterKeyboard(Key.X), new KeyCounterKeyboard(Key.X), new KeyCounterMouse(MouseButton.Left), new KeyCounterMouse(MouseButton.Right), @@ -44,10 +44,8 @@ namespace osu.Game.Tests.Visual.Gameplay Key key = (Key)((int)Key.A + RNG.Next(26)); kc.Add(new KeyCounterKeyboard(key)); }); - AddSliderStep("Fade time", 0, 200, 50, v => kc.FadeTime = v); Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key; - double time1 = 0; AddStep($"Press {testKey} key", () => { @@ -55,48 +53,17 @@ namespace osu.Game.Tests.Visual.Gameplay InputManager.ReleaseKey(testKey); }); - AddAssert($"Check {testKey} counter after keypress", () => rewindTestKeyCounterKeyboard.CountPresses == 1); + AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); AddStep($"Press {testKey} key", () => { InputManager.PressKey(testKey); InputManager.ReleaseKey(testKey); - time1 = Clock.CurrentTime; }); - AddAssert($"Check {testKey} counter after keypress", () => rewindTestKeyCounterKeyboard.CountPresses == 2); - - IFrameBasedClock oldClock = null; - - AddStep($"Rewind {testKey} counter once", () => - { - oldClock = rewindTestKeyCounterKeyboard.Clock; - rewindTestKeyCounterKeyboard.Clock = new FramedOffsetClock(new FixedClock(time1 - 10)); - }); - - AddAssert($"Check {testKey} counter after rewind", () => rewindTestKeyCounterKeyboard.CountPresses == 1); - - AddStep($"Rewind {testKey} counter to zero", () => rewindTestKeyCounterKeyboard.Clock = new FramedOffsetClock(new FixedClock(0))); - - AddAssert($"Check {testKey} counter after rewind", () => rewindTestKeyCounterKeyboard.CountPresses == 0); - - AddStep("Restore clock", () => rewindTestKeyCounterKeyboard.Clock = oldClock); + AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2); Add(kc); } - - private class FixedClock : IClock - { - private readonly double time; - - public FixedClock(double time) - { - this.time = time; - } - - public double CurrentTime => time; - public double Rate => 1; - public bool IsRunning => false; - } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 5808a78056..50583e43c4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -160,6 +160,15 @@ namespace osu.Game.Tests.Visual.Gameplay exitAndConfirm(); } + [Test] + public void TestRestartAfterResume() + { + pauseAndConfirm(); + resumeAndConfirm(); + restart(); + confirmExited(); + } + private void pauseAndConfirm() { pause(); @@ -198,6 +207,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("player exited", () => !Player.IsCurrentScreen()); } + private void restart() => AddStep("restart", () => Player.Restart()); private void pause() => AddStep("pause", () => Player.Pause()); private void resume() => AddStep("resume", () => Player.Resume()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index 3fbce9d43c..36335bc54a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -26,12 +26,14 @@ namespace osu.Game.Tests.Visual.Gameplay { AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0); AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0)); + AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail); } private class ScoreAccessibleReplayPlayer : ReplayPlayer { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new HUDOverlay HUDOverlay => base.HUDOverlay; + public new bool AllowFail => base.AllowFail; protected override bool PauseOnFocusLost => false; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index ee5552c6e0..b3d4820737 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -5,7 +5,9 @@ using System; using System.Globalization; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -133,11 +135,54 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1); } + [Test] + public void TestSwitchOff() + { + SkinConsumer consumer = null; + SwitchableSkinProvidingContainer target = null; + + AddStep("setup layout", () => + { + Child = new SkinSourceContainer + { + RelativeSizeAxes = Axes.Both, + Child = target = new SwitchableSkinProvidingContainer(new SecondarySource()) + { + RelativeSizeAxes = Axes.Both, + } + }; + }); + + AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true))); + AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox); + AddStep("disable", () => target.Disable()); + AddAssert("consumer using base source", () => consumer.Drawable is BaseSourceBox); + } + + private class SwitchableSkinProvidingContainer : SkinProvidingContainer + { + private bool allow = true; + + protected override bool AllowDrawableLookup(ISkinComponent component) => allow; + + public void Disable() + { + allow = false; + TriggerSourceChanged(); + } + + public SwitchableSkinProvidingContainer(ISkin skin) + : base(skin) + { + } + } + private class ExposedSkinnableDrawable : SkinnableDrawable { public new Drawable Drawable => base.Drawable; - public ExposedSkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + public ExposedSkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, + ConfineMode confineMode = ConfineMode.ScaleDownToFit) : base(new TestSkinComponent(name), defaultImplementation, allowFallback, confineMode) { } @@ -256,7 +301,7 @@ namespace osu.Game.Tests.Visual.Gameplay public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); + public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); } private class SecondarySource : ISkin @@ -267,10 +312,11 @@ namespace osu.Game.Tests.Visual.Gameplay public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); + public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); } - private class SkinSourceContainer : Container, ISkin + [Cached(typeof(ISkinSource))] + private class SkinSourceContainer : Container, ISkinSource { public Drawable GetDrawableComponent(ISkinComponent componentName) => new BaseSourceBox(); @@ -278,7 +324,9 @@ namespace osu.Game.Tests.Visual.Gameplay public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => throw new NotImplementedException(); + public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException(); + + public event Action SourceChanged; } private class TestSkinComponent : ISkinComponent diff --git a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs index f2718b8e80..681bf1b40b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Online.API; using osu.Game.Screens.Menu; using osu.Game.Users; @@ -10,23 +9,18 @@ namespace osu.Game.Tests.Visual.Menus { public class TestSceneDisclaimer : ScreenTestScene { - [Cached(typeof(IAPIProvider))] - private readonly DummyAPIAccess api = new DummyAPIAccess(); - [BackgroundDependencyLoader] private void load() { - Add(api); - AddStep("load disclaimer", () => LoadScreen(new Disclaimer())); AddStep("toggle support", () => { - api.LocalUser.Value = new User + API.LocalUser.Value = new User { - Username = api.LocalUser.Value.Username, - Id = api.LocalUser.Value.Id, - IsSupporter = !api.LocalUser.Value.IsSupporter, + Username = API.LocalUser.Value.Username, + Id = API.LocalUser.Value.Id, + IsSupporter = !API.LocalUser.Value.IsSupporter, }; }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index fa3c392b2e..7ba1782a28 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -14,6 +14,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMatchLeaderboard : MultiplayerTestScene { + protected override bool UseOnlineAPI => true; + public TestSceneMatchLeaderboard() { Room.RoomID.Value = 3; @@ -27,11 +29,8 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - [Resolved] - private IAPIProvider api { get; set; } - [BackgroundDependencyLoader] - private void load() + private void load(IAPIProvider api) { var req = new GetRoomScoresRequest(); req.Success += v => { }; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs index 069e133c2b..dfe61a4dda 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs @@ -12,6 +12,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [TestFixture] public class TestSceneMultiScreen : ScreenTestScene { + protected override bool UseOnlineAPI => true; + public override IReadOnlyList RequiredTypes => new[] { typeof(Screens.Multi.Multiplayer), diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs index 35449f5687..31eab7f74e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.AccountCreation; using osu.Game.Users; @@ -25,17 +25,16 @@ namespace osu.Game.Tests.Visual.Online typeof(AccountCreationScreen), }; - [Cached(typeof(IAPIProvider))] - private DummyAPIAccess api = new DummyAPIAccess(); + private readonly Container userPanelArea; + + private Bindable localUser; public TestSceneAccountCreationOverlay() { - Container userPanelArea; AccountCreationOverlay accountCreation; Children = new Drawable[] { - api, accountCreation = new AccountCreationOverlay(), userPanelArea = new Container { @@ -46,11 +45,18 @@ namespace osu.Game.Tests.Visual.Online }, }; - api.Logout(); - api.LocalUser.BindValueChanged(user => { userPanelArea.Child = new UserPanel(user.NewValue) { Width = 200 }; }, true); - AddStep("show", () => accountCreation.Show()); - AddStep("logout", () => api.Logout()); + } + + [BackgroundDependencyLoader] + private void load() + { + API.Logout(); + + localUser = API.LocalUser.GetBoundCopy(); + localUser.BindValueChanged(user => { userPanelArea.Child = new UserPanel(user.NewValue) { Width = 200 }; }, true); + + AddStep("logout", API.Logout); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index ee9e088dcc..9f03d947b9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -42,6 +42,8 @@ namespace osu.Game.Tests.Visual.Online typeof(BeatmapAvailability), }; + protected override bool UseOnlineAPI => true; + private RulesetInfo taikoRuleset; private RulesetInfo maniaRuleset; diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index cf8bac7642..658f678b10 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -27,6 +27,8 @@ namespace osu.Game.Tests.Visual.Online typeof(Comments), }; + protected override bool UseOnlineAPI => true; + protected override void LoadComplete() { base.LoadComplete(); @@ -66,6 +68,34 @@ namespace osu.Game.Tests.Visual.Online changelog.ShowListing(); changelog.Show(); }); + + AddStep(@"Ensure HTML string unescaping", () => + { + changelog.ShowBuild(new APIChangelogBuild + { + Version = "2019.920.0", + DisplayVersion = "2019.920.0", + UpdateStream = new APIUpdateStream + { + Name = "Test", + DisplayName = "Test" + }, + ChangelogEntries = new List + { + new APIChangelogEntry + { + Category = "Testing HTML strings unescaping", + Title = "Ensuring HTML strings are being unescaped", + MessageHtml = """"This text should appear triple-quoted""" >_<", + GithubUser = new APIChangelogUser + { + DisplayName = "Dummy", + OsuUsername = "Dummy", + } + }, + } + }); + }); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs new file mode 100644 index 0000000000..4773e84a5e --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osu.Game.Online.Chat; +using osu.Game.Overlays.Chat; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneChatLineTruncation : OsuTestScene + { + private readonly TestChatLineContainer textContainer; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ChatLine), + typeof(Message), + typeof(LinkFlowContainer), + typeof(MessageFormatter) + }; + + public TestSceneChatLineTruncation() + { + Add(textContainer = new TestChatLineContainer + { + Padding = new MarginPadding { Left = 20, Right = 20 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }); + } + + [BackgroundDependencyLoader] + private void load() + { + testFormatting(); + } + + private void clear() => AddStep("clear messages", textContainer.Clear); + + private void addMessageWithChecks(string text, bool isAction = false, bool isImportant = false, string username = null) + { + int index = textContainer.Count + 1; + var newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index, username)); + textContainer.Add(newLine); + } + + private void testFormatting() + { + for (int a = 0; a < 25; a++) + addMessageWithChecks($"Wide {a} character username.", username: new string('w', a)); + addMessageWithChecks("Short name with spaces.", username: "sho rt name"); + addMessageWithChecks("Long name with spaces.", username: "long name with s p a c e s"); + } + + private class DummyMessage : Message + { + private static long messageCounter; + + internal static readonly User TEST_SENDER_BACKGROUND = new User + { + Username = @"i-am-important", + Id = 42, + Colour = "#250cc9", + }; + + internal static readonly User TEST_SENDER = new User + { + Username = @"Somebody", + Id = 1, + }; + + public new DateTimeOffset Timestamp = DateTimeOffset.Now; + + public DummyMessage(string text, bool isAction = false, bool isImportant = false, int number = 0, string username = null) + : base(messageCounter++) + { + Content = text; + IsAction = isAction; + Sender = new User + { + Username = username ?? $"user {number}", + Id = number, + Colour = isImportant ? "#250cc9" : null, + }; + } + } + + private class TestChatLineContainer : FillFlowContainer + { + protected override int Compare(Drawable x, Drawable y) + { + var xC = (ChatLine)x; + var yC = (ChatLine)y; + + return xC.Message.CompareTo(yC.Message); + } + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index c18e0e3064..a1c77e2db0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -127,6 +127,9 @@ namespace osu.Game.Tests.Visual.Online addMessageWithChecks("is now playing [https://osu.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap); addMessageWithChecks("Let's (try)[https://osu.ppy.sh/home] [https://osu.ppy.sh/b/252238 multiple links] https://osu.ppy.sh/home", 3, expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External }); + addMessageWithChecks("[https://osu.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://osu.ppy.sh/home)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://osu.ppy.sh/home] and [[also a rogue wiki link]]", 2, expectedActions: new[] { LinkAction.External, LinkAction.External }); // note that there's 0 links here (they get removed if a channel is not found) addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present)."); addMessageWithChecks("I am important!", 0, false, true); diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs index 75c2a2a6a1..d9873ea243 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs @@ -13,6 +13,8 @@ namespace osu.Game.Tests.Visual.Online { private DirectOverlay direct; + protected override bool UseOnlineAPI => true; + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs index 838347800f..d3b037f499 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs @@ -17,14 +17,15 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneHistoricalSection : OsuTestScene { - public override IReadOnlyList RequiredTypes => - new[] - { - typeof(HistoricalSection), - typeof(PaginatedMostPlayedBeatmapContainer), - typeof(DrawableMostPlayedBeatmap), - typeof(DrawableProfileRow) - }; + protected override bool UseOnlineAPI => true; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(HistoricalSection), + typeof(PaginatedMostPlayedBeatmapContainer), + typeof(DrawableMostPlayedBeatmap), + typeof(DrawableProfileRow) + }; public TestSceneHistoricalSection() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsDismissableFlag.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsDismissableFlag.cs new file mode 100644 index 0000000000..db6afa9bf3 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsDismissableFlag.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Overlays.Rankings; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneRankingsDismissableFlag : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DismissableFlag), + }; + + public TestSceneRankingsDismissableFlag() + { + DismissableFlag flag; + SpriteText text; + + var countryA = new Country + { + FlagName = "BY", + FullName = "Belarus" + }; + + var countryB = new Country + { + FlagName = "US", + FullName = "United States" + }; + + AddRange(new Drawable[] + { + flag = new DismissableFlag + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(30, 20), + Country = countryA, + }, + text = new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Invoked", + Font = OsuFont.GetFont(size: 30), + Alpha = 0, + } + }); + + flag.Action += () => text.FadeIn().Then().FadeOut(1000, Easing.OutQuint); + + AddStep("Trigger click", () => flag.Click()); + AddStep("Change to country 2", () => flag.Country = countryB); + AddStep("Change to country 1", () => flag.Country = countryA); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs new file mode 100644 index 0000000000..c0da605cdb --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs @@ -0,0 +1,77 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Overlays.Rankings; +using osu.Game.Rulesets; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneRankingsHeader : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DismissableFlag), + typeof(HeaderTitle), + typeof(RankingsRulesetSelector), + typeof(RankingsScopeSelector), + typeof(RankingsHeader), + }; + + public TestSceneRankingsHeader() + { + var countryBindable = new Bindable(); + var ruleset = new Bindable(); + var scope = new Bindable(); + + Add(new RankingsHeader + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scope = { BindTarget = scope }, + Country = { BindTarget = countryBindable }, + Ruleset = { BindTarget = ruleset }, + Spotlights = new[] + { + new Spotlight + { + Id = 1, + Text = "Spotlight 1" + }, + new Spotlight + { + Id = 2, + Text = "Spotlight 2" + }, + new Spotlight + { + Id = 3, + Text = "Spotlight 3" + } + } + }); + + var country = new Country + { + FlagName = "BY", + FullName = "Belarus" + }; + + var unknownCountry = new Country + { + FlagName = "CK", + FullName = "Cook Islands" + }; + + AddStep("Set country", () => countryBindable.Value = country); + AddAssert("Check scope is Performance", () => scope.Value == RankingsScope.Performance); + AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score); + AddAssert("Check country is Null", () => countryBindable.Value == null); + AddStep("Set country with no flag", () => countryBindable.Value = unknownCountry); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeaderTitle.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeaderTitle.cs new file mode 100644 index 0000000000..849ca2defc --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeaderTitle.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Overlays.Rankings; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneRankingsHeaderTitle : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DismissableFlag), + typeof(HeaderTitle), + }; + + public TestSceneRankingsHeaderTitle() + { + var countryBindable = new Bindable(); + var scope = new Bindable(); + + Add(new HeaderTitle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Country = { BindTarget = countryBindable }, + Scope = { BindTarget = scope }, + }); + + var countryA = new Country + { + FlagName = "BY", + FullName = "Belarus" + }; + + var countryB = new Country + { + FlagName = "US", + FullName = "United States" + }; + + AddStep("Set country", () => countryBindable.Value = countryA); + AddAssert("Check scope is Performance", () => scope.Value == RankingsScope.Performance); + AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score); + AddAssert("Check country is Null", () => countryBindable.Value == null); + + AddStep("Set country 1", () => countryBindable.Value = countryA); + AddStep("Set country 2", () => countryBindable.Value = countryB); + AddStep("Set null country", () => countryBindable.Value = null); + AddStep("Set scope to Performance", () => scope.Value = RankingsScope.Performance); + AddStep("Set scope to Spotlights", () => scope.Value = RankingsScope.Spotlights); + AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score); + AddStep("Set scope to Country", () => scope.Value = RankingsScope.Country); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsRulesetSelector.cs new file mode 100644 index 0000000000..84515bd3a4 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsRulesetSelector.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Overlays.Rankings; +using osu.Framework.Graphics; +using osu.Game.Rulesets; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Catch; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneRankingsRulesetSelector : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(RankingsRulesetSelector), + }; + + public TestSceneRankingsRulesetSelector() + { + var current = new Bindable(); + + Add(new RankingsRulesetSelector + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { BindTarget = current } + }); + + AddStep("Select osu!", () => current.Value = new OsuRuleset().RulesetInfo); + AddStep("Select mania", () => current.Value = new ManiaRuleset().RulesetInfo); + AddStep("Select taiko", () => current.Value = new TaikoRuleset().RulesetInfo); + AddStep("Select catch", () => current.Value = new CatchRuleset().RulesetInfo); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsScopeSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsScopeSelector.cs new file mode 100644 index 0000000000..3693d6b5b4 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsScopeSelector.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Game.Overlays.Rankings; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneRankingsScopeSelector : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(RankingsScopeSelector), + }; + + private readonly Box background; + + public TestSceneRankingsScopeSelector() + { + var scope = new Bindable(); + + AddRange(new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new RankingsScopeSelector + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = scope, + } + }); + + AddStep(@"Select country", () => scope.Value = RankingsScope.Country); + AddStep(@"Select performance", () => scope.Value = RankingsScope.Performance); + AddStep(@"Select score", () => scope.Value = RankingsScope.Score); + AddStep(@"Select spotlights", () => scope.Value = RankingsScope.Spotlights); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.GreySeafoam; + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs index 5cb96c7ed2..dbd7544b38 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs @@ -13,6 +13,8 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneSocialOverlay : OsuTestScene { + protected override bool UseOnlineAPI => true; + public override IReadOnlyList RequiredTypes => new[] { typeof(UserPanel), diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 2285c9b799..63b8acb234 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -17,6 +17,8 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneUserProfileHeader : OsuTestScene { + protected override bool UseOnlineAPI => true; + public override IReadOnlyList RequiredTypes => new[] { typeof(ProfileHeader), diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 84c99d8c3a..93e6607ac5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -19,6 +19,8 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserProfileOverlay : OsuTestScene { + protected override bool UseOnlineAPI => true; + private readonly TestUserProfileOverlay profile; [Resolved] diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs b/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs index 9f0a8c769a..2951f6b63e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs @@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserRanks : OsuTestScene { + protected override bool UseOnlineAPI => true; + public override IReadOnlyList RequiredTypes => new[] { typeof(DrawableProfileScore), typeof(RanksSection) }; public TestSceneUserRanks() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 6669ec7da3..71399106f4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -242,6 +242,21 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); AddAssert("Selection is non-null", () => currentSelection != null); + + setSelected(1, 3); + AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria + { + SearchText = "#3", + StarDifficulty = new FilterCriteria.OptionalRange + { + Min = 2, + Max = 5.5, + IsLowerInclusive = true + } + }, false)); + checkSelected(3, 2); + + AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); } /// diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs index 7b97a27732..ed9e01a67e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Screens.Select; +using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.SongSelect @@ -30,45 +31,44 @@ namespace osu.Game.Tests.Visual.SongSelect Size = new Vector2(550f, 450f), }); - AddStep("all metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) + AddStep("all metrics", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = { - BeatmapSetInfo = + BeatmapSet = new BeatmapSetInfo { Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } }, - BeatmapInfo = + Version = "All Metrics", + Metadata = new BeatmapMetadata { - Version = "All Metrics", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has all the metrics", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 7, - DrainRate = 1, - OverallDifficulty = 5.7f, - ApproachRate = 3.5f, - }, - StarDifficulty = 5.3f, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - } + Source = "osu!lazer", + Tags = "this beatmap has all the metrics", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 7, + DrainRate = 1, + OverallDifficulty = 5.7f, + ApproachRate = 3.5f, + }, + StarDifficulty = 5.3f, + Metrics = new BeatmapMetrics + { + Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), + Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), + }, } - ); + })); - AddStep("all except source", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) + AddStep("all except source", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap { - BeatmapSetInfo = - { - Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } - }, BeatmapInfo = { + BeatmapSet = new BeatmapSetInfo + { + Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } + }, Version = "All Metrics", Metadata = new BeatmapMetadata { @@ -88,16 +88,16 @@ namespace osu.Game.Tests.Visual.SongSelect Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), }, } - }); + })); - AddStep("ratings", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) + AddStep("ratings", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap { - BeatmapSetInfo = - { - Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } - }, BeatmapInfo = { + BeatmapSet = new BeatmapSetInfo + { + Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } + }, Version = "Only Ratings", Metadata = new BeatmapMetadata { @@ -113,9 +113,9 @@ namespace osu.Game.Tests.Visual.SongSelect }, StarDifficulty = 4.8f } - }); + })); - AddStep("fails+retries", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) + AddStep("fails+retries", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap { BeatmapInfo = { @@ -139,9 +139,9 @@ namespace osu.Game.Tests.Visual.SongSelect Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), }, } - }); + })); - AddStep("null metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) + AddStep("null metrics", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap { BeatmapInfo = { @@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual.SongSelect }, StarDifficulty = 1.97f, } - }); + })); AddStep("null beatmap", () => detailsArea.Beatmap = null); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs similarity index 86% rename from osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs rename to osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 8e358a77db..fb27ec7654 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; @@ -14,19 +16,20 @@ using osuTK; namespace osu.Game.Tests.Visual.SongSelect { - [Description("PlaySongSelect leaderboard")] - public class TestSceneLeaderboard : OsuTestScene + public class TestSceneBeatmapLeaderboard : OsuTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(Placeholder), typeof(MessagePlaceholder), typeof(RetrievalFailurePlaceholder), + typeof(UserTopScoreContainer), + typeof(Leaderboard), }; private readonly FailableLeaderboard leaderboard; - public TestSceneLeaderboard() + public TestSceneBeatmapLeaderboard() { Add(leaderboard = new FailableLeaderboard { @@ -37,15 +40,43 @@ namespace osu.Game.Tests.Visual.SongSelect }); AddStep(@"New Scores", newScores); + AddStep(@"Show personal best", showPersonalBest); AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores)); AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure)); AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter)); AddStep(@"Not logged in", () => leaderboard.SetRetrievalState(PlaceholderState.NotLoggedIn)); AddStep(@"Unavailable", () => leaderboard.SetRetrievalState(PlaceholderState.Unavailable)); + AddStep(@"None selected", () => leaderboard.SetRetrievalState(PlaceholderState.NoneSelected)); foreach (BeatmapSetOnlineStatus status in Enum.GetValues(typeof(BeatmapSetOnlineStatus))) AddStep($"{status} beatmap", () => showBeatmapWithStatus(status)); } + private void showPersonalBest() + { + leaderboard.TopScore = new APILegacyUserTopScoreInfo + { + Position = 999, + Score = new APILegacyScoreInfo + { + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + User = new User + { + Id = 6602580, + Username = @"waaiiru", + Country = new Country + { + FullName = @"Spain", + FlagName = @"ES", + }, + }, + } + }; + } + private void newScores() { var scores = new[] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs new file mode 100644 index 0000000000..7fac45e0f1 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs @@ -0,0 +1,119 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Scoring; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public class TestSceneUserTopScoreContainer : OsuTestScene + { + public TestSceneUserTopScoreContainer() + { + UserTopScoreContainer topScoreContainer; + + Add(new Container + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = 500, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.DarkGreen, + }, + topScoreContainer = new UserTopScoreContainer + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + } + } + }); + + var scores = new[] + { + new APILegacyUserTopScoreInfo + { + Position = 999, + Score = new APILegacyScoreInfo + { + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + User = new User + { + Id = 6602580, + Username = @"waaiiru", + Country = new Country + { + FullName = @"Spain", + FlagName = @"ES", + }, + }, + } + }, + new APILegacyUserTopScoreInfo + { + Position = 110000, + Score = new APILegacyScoreInfo + { + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + User = new User + { + Id = 4608074, + Username = @"Skycries", + Country = new Country + { + FullName = @"Brazil", + FlagName = @"BR", + }, + }, + } + }, + new APILegacyUserTopScoreInfo + { + Position = 22333, + Score = new APILegacyScoreInfo + { + Rank = ScoreRank.S, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + User = new User + { + Id = 1541390, + Username = @"Toukai", + Country = new Country + { + FullName = @"Canada", + FlagName = @"CA", + }, + }, + } + } + }; + + AddStep(@"Trigger visibility", topScoreContainer.ToggleVisibility); + AddStep(@"Add score(rank 999)", () => topScoreContainer.Score.Value = scores[0]); + AddStep(@"Add score(rank 110000)", () => topScoreContainer.Score.Value = scores[1]); + AddStep(@"Add score(rank 22333)", () => topScoreContainer.Score.Value = scores[2]); + AddStep(@"Add null score", () => topScoreContainer.Score.Value = null); + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs index a6ff3462d4..cc4a57fb83 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.UserInterface Add(overlay = new DialogOverlay()); - AddStep("dialog #1", () => overlay.Push(new PopupDialog + AddStep("dialog #1", () => overlay.Push(new TestPopupDialog { Icon = FontAwesome.Regular.TrashAlt, HeaderText = @"Confirm deletion of", @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, })); - AddStep("dialog #2", () => overlay.Push(new PopupDialog + AddStep("dialog #2", () => overlay.Push(new TestPopupDialog { Icon = FontAwesome.Solid.Cog, HeaderText = @"What do you want to do with", @@ -71,5 +71,9 @@ namespace osu.Game.Tests.Visual.UserInterface }, })); } + + private class TestPopupDialog : PopupDialog + { + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs index f787754aa4..feef1dae6b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs @@ -52,19 +52,23 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("start confirming", () => overlay.Begin()); AddStep("abort confirming", () => overlay.Abort()); + AddAssert("ensure not fired internally", () => !overlay.Fired); AddAssert("ensure aborted", () => !fired); AddStep("start confirming", () => overlay.Begin()); AddUntilStep("wait until confirmed", () => fired); + AddAssert("ensure fired internally", () => overlay.Fired); + + AddStep("abort after fire", () => overlay.Abort()); + AddAssert("ensure not fired internally", () => !overlay.Fired); + AddStep("start confirming", () => overlay.Begin()); + AddUntilStep("wait until fired again", () => overlay.Fired); } private class TestHoldToConfirmOverlay : ExitConfirmOverlay { - protected override bool AllowMultipleFires => true; - public void Begin() => BeginConfirm(); - public void Abort() => AbortConfirm(); } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs new file mode 100644 index 0000000000..700adad9cb --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneLabelledComponent : OsuTestScene + { + [TestCase(false)] + [TestCase(true)] + public void TestPadded(bool hasDescription) => createPaddedComponent(hasDescription); + + [TestCase(false)] + [TestCase(true)] + public void TestNonPadded(bool hasDescription) => createPaddedComponent(hasDescription, false); + + private void createPaddedComponent(bool hasDescription = false, bool padded = true) + { + AddStep("create component", () => + { + LabelledComponent component; + + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Child = component = padded ? (LabelledComponent)new PaddedLabelledComponent() : new NonPaddedLabelledComponent(), + }; + + component.Label = "a sample component"; + component.Description = hasDescription ? "this text describes the component" : string.Empty; + }); + } + + private class PaddedLabelledComponent : LabelledComponent + { + public PaddedLabelledComponent() + : base(true) + { + } + + protected override Drawable CreateComponent() => new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Red, + Text = @"(( Component ))" + }; + } + + private class NonPaddedLabelledComponent : LabelledComponent + { + public NonPaddedLabelledComponent() + : base(false) + { + } + + protected override Drawable CreateComponent() => new Container + { + RelativeSizeAxes = Axes.X, + Height = 40, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.SlateGray + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.Red, + Text = @"(( Component ))" + } + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs index 395905a30d..53a2bfabbc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs @@ -7,7 +7,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Screens.Edit.Setup.Components.LabelledComponents; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Tests.Visual.UserInterface { @@ -19,6 +20,36 @@ namespace osu.Game.Tests.Visual.UserInterface typeof(LabelledTextBox), }; + [TestCase(false)] + [TestCase(true)] + public void TestTextBox(bool hasDescription) => createTextBox(hasDescription); + + private void createTextBox(bool hasDescription = false) + { + AddStep("create component", () => + { + LabelledComponent component; + + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Child = component = new LabelledTextBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Label = "Testing text", + PlaceholderText = "This is definitely working as intended", + } + }; + + component.Label = "a sample component"; + component.Description = hasDescription ? "this text describes the component" : string.Empty; + }); + } + [BackgroundDependencyLoader] private void load() { @@ -32,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface { Anchor = Anchor.Centre, Origin = Anchor.Centre, - LabelText = "Testing text", + Label = "Testing text", PlaceholderText = "This is definitely working as intended", } }; diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs index 9ddd8f4038..3d39bb7003 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs @@ -13,13 +13,22 @@ namespace osu.Game.Tests.Visual.UserInterface { public TestScenePopupDialog() { - var popup = new PopupDialog + Add(new TestPopupDialog { RelativeSizeAxes = Axes.Both, State = { Value = Framework.Graphics.Containers.Visibility.Visible }, - Icon = FontAwesome.Solid.AssistiveListeningSystems, - HeaderText = @"This is a test popup", - BodyText = "I can say lots of stuff and even wrap my words!", + }); + } + + private class TestPopupDialog : PopupDialog + { + public TestPopupDialog() + { + Icon = FontAwesome.Solid.AssistiveListeningSystems; + + HeaderText = @"This is a test popup"; + BodyText = "I can say lots of stuff and even wrap my words!"; + Buttons = new PopupDialogButton[] { new PopupDialogCancelButton @@ -30,10 +39,8 @@ namespace osu.Game.Tests.Visual.UserInterface { Text = @"You're a fake!", }, - } - }; - - Add(popup); + }; + } } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs index 9cdfcb6cc4..198cc70e01 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs @@ -20,6 +20,8 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneUpdateableBeatmapBackgroundSprite : OsuTestScene { + protected override bool UseOnlineAPI => true; + private BeatmapSetInfo testBeatmap; private IAPIProvider api; private RulesetStore rulesets; diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index 3e0df8d45e..db9576b5fa 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO.Archives; @@ -42,6 +43,8 @@ namespace osu.Game.Tests protected override Texture GetBackground() => null; + protected override VideoSprite GetVideo() => null; + protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Track GetTrack() => trackStore.Get(firstAudioFile); diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 84f67c9319..75e6354612 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs new file mode 100644 index 0000000000..650b4c5412 --- /dev/null +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Tournament.Screens; + +namespace osu.Game.Tournament.Tests.Screens +{ + public class TestSceneSetupScreen : TournamentTestScene + { + [BackgroundDependencyLoader] + private void load() + { + Add(new SetupScreen()); + } + } +} diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index bba3c92245..491cf54686 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 4fd858bd12..e05d96e098 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Platform.Windows; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Online.API; @@ -26,103 +27,120 @@ namespace osu.Game.Tournament.IPC [Resolved] protected RulesetStore Rulesets { get; private set; } + [Resolved] + private GameHost host { get; set; } + + [Resolved] + private LadderInfo ladder { get; set; } + private int lastBeatmapId; + private ScheduledDelegate scheduled; + + public Storage Storage { get; private set; } [BackgroundDependencyLoader] - private void load(LadderInfo ladder, GameHost host) + private void load() { - StableStorage stable; + LocateStableStorage(); + } + + public Storage LocateStableStorage() + { + scheduled?.Cancel(); + + Storage = null; try { - stable = new StableStorage(host as DesktopGameHost); + Storage = new StableStorage(host as DesktopGameHost); + + const string file_ipc_filename = "ipc.txt"; + const string file_ipc_state_filename = "ipc-state.txt"; + const string file_ipc_scores_filename = "ipc-scores.txt"; + const string file_ipc_channel_filename = "ipc-channel.txt"; + + if (Storage.Exists(file_ipc_filename)) + scheduled = Scheduler.AddDelayed(delegate + { + try + { + using (var stream = Storage.GetStream(file_ipc_filename)) + using (var sr = new StreamReader(stream)) + { + var beatmapId = int.Parse(sr.ReadLine()); + var mods = int.Parse(sr.ReadLine()); + + if (lastBeatmapId != beatmapId) + { + lastBeatmapId = beatmapId; + + var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.BeatmapInfo != null); + + if (existing != null) + Beatmap.Value = existing.BeatmapInfo; + else + { + var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId }); + req.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets); + API.Queue(req); + } + } + + Mods.Value = (LegacyMods)mods; + } + } + catch + { + // file might be in use. + } + + try + { + using (var stream = Storage.GetStream(file_ipc_channel_filename)) + using (var sr = new StreamReader(stream)) + { + ChatChannel.Value = sr.ReadLine(); + } + } + catch (Exception) + { + // file might be in use. + } + + try + { + using (var stream = Storage.GetStream(file_ipc_state_filename)) + using (var sr = new StreamReader(stream)) + { + State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine()); + } + } + catch (Exception) + { + // file might be in use. + } + + try + { + using (var stream = Storage.GetStream(file_ipc_scores_filename)) + using (var sr = new StreamReader(stream)) + { + Score1.Value = int.Parse(sr.ReadLine()); + Score2.Value = int.Parse(sr.ReadLine()); + } + } + catch (Exception) + { + // file might be in use. + } + }, 250, true); } catch (Exception e) { Logger.Error(e, "Stable installation could not be found; disabling file based IPC"); - return; } - const string file_ipc_filename = "ipc.txt"; - const string file_ipc_state_filename = "ipc-state.txt"; - const string file_ipc_scores_filename = "ipc-scores.txt"; - const string file_ipc_channel_filename = "ipc-channel.txt"; - - if (stable.Exists(file_ipc_filename)) - Scheduler.AddDelayed(delegate - { - try - { - using (var stream = stable.GetStream(file_ipc_filename)) - using (var sr = new StreamReader(stream)) - { - var beatmapId = int.Parse(sr.ReadLine()); - var mods = int.Parse(sr.ReadLine()); - - if (lastBeatmapId != beatmapId) - { - lastBeatmapId = beatmapId; - - var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.BeatmapInfo != null); - - if (existing != null) - Beatmap.Value = existing.BeatmapInfo; - else - { - var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId }); - req.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets); - API.Queue(req); - } - } - - Mods.Value = (LegacyMods)mods; - } - } - catch - { - // file might be in use. - } - - try - { - using (var stream = stable.GetStream(file_ipc_channel_filename)) - using (var sr = new StreamReader(stream)) - { - ChatChannel.Value = sr.ReadLine(); - } - } - catch (Exception) - { - // file might be in use. - } - - try - { - using (var stream = stable.GetStream(file_ipc_state_filename)) - using (var sr = new StreamReader(stream)) - { - State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine()); - } - } - catch (Exception) - { - // file might be in use. - } - - try - { - using (var stream = stable.GetStream(file_ipc_scores_filename)) - using (var sr = new StreamReader(stream)) - { - Score1.Value = int.Parse(sr.ReadLine()); - Score2.Value = int.Parse(sr.ReadLine()); - } - } - catch (Exception) - { - // file might be in use. - } - }, 250, true); + return Storage; } /// diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs new file mode 100644 index 0000000000..091a837745 --- /dev/null +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osu.Game.Tournament.IPC; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tournament.Screens +{ + public class SetupScreen : TournamentScreen, IProvideVideo + { + private FillFlowContainer fillFlow; + + private LoginOverlay loginOverlay; + + [Resolved] + private MatchIPCInfo ipc { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = fillFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(10), + Spacing = new Vector2(10), + }; + + api.LocalUser.BindValueChanged(_ => Schedule(reload)); + reload(); + } + + private void reload() + { + var fileBasedIpc = ipc as FileBasedIPC; + + fillFlow.Children = new Drawable[] + { + new ActionableInfo + { + Label = "Current IPC source", + ButtonText = "Refresh", + Action = () => + { + fileBasedIpc?.LocateStableStorage(); + reload(); + }, + Value = fileBasedIpc?.Storage?.GetFullPath(string.Empty) ?? "Not found", + Failing = fileBasedIpc?.Storage == null, + Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation, and that it is registered as the default osu! install." + }, + new ActionableInfo + { + Label = "Current User", + ButtonText = "Change Login", + Action = () => + { + api.Logout(); + + if (loginOverlay == null) + { + AddInternal(loginOverlay = new LoginOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }); + } + + loginOverlay.State.Value = Visibility.Visible; + }, + Value = api?.LocalUser.Value.Username, + Failing = api?.IsLoggedIn != true, + Description = "In order to access the API and display metadata, a login is required." + } + }; + } + + private class ActionableInfo : LabelledComponent + { + private OsuButton button; + + public ActionableInfo() + : base(true) + { + } + + public string ButtonText + { + set => button.Text = value; + } + + public string Value + { + set => valueText.Text = value; + } + + public bool Failing + { + set => valueText.Colour = value ? Color4.Red : Color4.White; + } + + public Action Action; + + private OsuSpriteText valueText; + + protected override Drawable CreateComponent() => new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] + { + valueText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + button = new TriangleButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(100, 30), + Action = () => Action?.Invoke() + }, + } + }; + } + } +} diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 4c255be463..02ee1c8603 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -69,6 +69,7 @@ namespace osu.Game.Tournament RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + new SetupScreen(), new ScheduleScreen(), new LadderScreen(), new LadderEditorScreen(), @@ -106,6 +107,8 @@ namespace osu.Game.Tournament Direction = FillDirection.Vertical, Children = new Drawable[] { + new OsuButton { RelativeSizeAxes = Axes.X, Text = "Setup", Action = () => SetScreen(typeof(SetupScreen)) }, + new Container { RelativeSizeAxes = Axes.X, Height = 50 }, new OsuButton { RelativeSizeAxes = Axes.X, Text = "Team Editor", Action = () => SetScreen(typeof(TeamEditorScreen)) }, new OsuButton { RelativeSizeAxes = Axes.X, Text = "Rounds Editor", Action = () => SetScreen(typeof(RoundEditorScreen)) }, new OsuButton { RelativeSizeAxes = Axes.X, Text = "Bracket Editor", Action = () => SetScreen(typeof(LadderEditorScreen)) }, @@ -127,7 +130,7 @@ namespace osu.Game.Tournament }, }; - SetScreen(typeof(ScheduleScreen)); + SetScreen(typeof(SetupScreen)); } public void SetScreen(Type screenType) diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj index 4790fcbcde..bddaff0a80 100644 --- a/osu.Game.Tournament/osu.Game.Tournament.csproj +++ b/osu.Game.Tournament/osu.Game.Tournament.csproj @@ -11,6 +11,6 @@ - + \ No newline at end of file diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 166ba5111c..55b8b80e44 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; @@ -88,7 +89,7 @@ namespace osu.Game.Beatmaps protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) - beatmapSet.Beatmaps = createBeatmapDifficulties(archive); + beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); foreach (BeatmapInfo b in beatmapSet.Beatmaps) { @@ -278,13 +279,13 @@ namespace osu.Game.Beatmaps /// /// Create all required s for the provided archive. /// - private List createBeatmapDifficulties(ArchiveReader reader) + private List createBeatmapDifficulties(List files) { var beatmapInfos = new List(); - foreach (var name in reader.Filenames.Where(f => f.EndsWith(".osu"))) + foreach (var file in files.Where(f => f.Filename.EndsWith(".osu"))) { - using (var raw = reader.GetStream(name)) + using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) using (var ms = new MemoryStream()) //we need a memory stream so we can seek using (var sr = new StreamReader(ms)) { @@ -294,12 +295,13 @@ namespace osu.Game.Beatmaps var decoder = Decoder.GetDecoder(sr); IBeatmap beatmap = decoder.Decode(sr); - beatmap.BeatmapInfo.Path = name; + beatmap.BeatmapInfo.Path = file.Filename; beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); beatmap.BeatmapInfo.Ruleset = ruleset; + // TODO: this should be done in a better place once we actually need to dynamically update it. beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.Length = calculateLength(beatmap); @@ -340,6 +342,7 @@ namespace osu.Game.Beatmaps protected override IBeatmap GetBeatmap() => beatmap; protected override Texture GetBackground() => null; + protected override VideoSprite GetVideo() => null; protected override Track GetTrack() => null; } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 5bbffc2f77..1d00c94ef2 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Game.Beatmaps.Formats; @@ -64,6 +65,21 @@ namespace osu.Game.Beatmaps } } + protected override VideoSprite GetVideo() + { + if (Metadata?.VideoFile == null) + return null; + + try + { + return new VideoSprite(textureStore.GetStream(getPathForFile(Metadata.VideoFile))); + } + catch + { + return null; + } + } + protected override Track GetTrack() { try diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 001f319307..9267527d79 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -52,6 +52,7 @@ namespace osu.Game.Beatmaps public int PreviewTime { get; set; } public string AudioFile { get; set; } public string BackgroundFile { get; set; } + public string VideoFile { get; set; } public override string ToString() => $"{Artist} - {Title} ({Author})"; @@ -81,7 +82,8 @@ namespace osu.Game.Beatmaps && Tags == other.Tags && PreviewTime == other.PreviewTime && AudioFile == other.AudioFile - && BackgroundFile == other.BackgroundFile; + && BackgroundFile == other.BackgroundFile + && VideoFile == other.VideoFile; } } } diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 81f517dd86..8014631eca 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -84,7 +84,7 @@ namespace osu.Game.Beatmaps.Drawables Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) - Icon = ruleset?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } + Icon = ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } } }; } diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 29ade24328..a3ab01c886 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -7,6 +7,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -44,6 +45,8 @@ namespace osu.Game.Beatmaps protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4"); + protected override VideoSprite GetVideo() => null; + protected override Track GetTrack() => GetVirtualTrack(); private class DummyRulesetInfo : RulesetInfo diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 02d969b571..0532790f0a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -296,8 +296,13 @@ namespace osu.Game.Beatmaps.Formats switch (type) { case EventType.Background: - string filename = split[2].Trim('"'); - beatmap.BeatmapInfo.Metadata.BackgroundFile = FileSafety.PathStandardise(filename); + string bgFilename = split[2].Trim('"'); + beatmap.BeatmapInfo.Metadata.BackgroundFile = FileSafety.PathStandardise(bgFilename); + break; + + case EventType.Video: + string videoFilename = split[2].Trim('"'); + beatmap.BeatmapInfo.Metadata.VideoFile = FileSafety.PathStandardise(videoFilename); break; case EventType.Break: diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 44071d9cc1..a087a52ada 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -25,6 +26,11 @@ namespace osu.Game.Beatmaps /// Texture Background { get; } + /// + /// Retrieves the video background file for this . + /// + VideoSprite Video { get; } + /// /// Retrieves the audio track for this . /// diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index d8ab411beb..3fc33e9f52 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osu.Game.Skinning; +using osu.Framework.Graphics.Video; namespace osu.Game.Beatmaps { @@ -186,6 +187,10 @@ namespace osu.Game.Beatmaps protected abstract Texture GetBackground(); private readonly RecyclableLazy background; + public VideoSprite Video => GetVideo(); + + protected abstract VideoSprite GetVideo(); + public bool TrackLoaded => track.IsResultAvailable; public Track Track => track.Value; protected abstract Track GetTrack(); diff --git a/osu.Game/Configuration/DatabasedConfigManager.cs b/osu.Game/Configuration/DatabasedConfigManager.cs index d5cdd7e4bc..02382cfd2b 100644 --- a/osu.Game/Configuration/DatabasedConfigManager.cs +++ b/osu.Game/Configuration/DatabasedConfigManager.cs @@ -16,11 +16,11 @@ namespace osu.Game.Configuration private readonly int? variant; - private readonly List databasedSettings; + private List databasedSettings; private readonly RulesetInfo ruleset; - private readonly bool legacySettingsExist; + private bool legacySettingsExist; protected DatabasedConfigManager(SettingsStore settings, RulesetInfo ruleset = null, int? variant = null) { @@ -28,21 +28,31 @@ namespace osu.Game.Configuration this.ruleset = ruleset; this.variant = variant; - databasedSettings = settings.Query(ruleset?.ID, variant); - legacySettingsExist = databasedSettings.Any(s => int.TryParse(s.Key, out var _)); + Load(); InitialiseDefaults(); } protected override void PerformLoad() { + databasedSettings = settings.Query(ruleset?.ID, variant); + legacySettingsExist = databasedSettings.Any(s => int.TryParse(s.Key, out var _)); } protected override bool PerformSave() { + lock (dirtySettings) + { + foreach (var setting in dirtySettings) + settings.Update(setting); + dirtySettings.Clear(); + } + return true; } + private readonly List dirtySettings = new List(); + protected override void AddBindable(T lookup, Bindable bindable) { base.AddBindable(lookup, bindable); @@ -80,7 +90,12 @@ namespace osu.Game.Configuration bindable.ValueChanged += b => { setting.Value = b.NewValue; - settings.Update(setting); + + lock (dirtySettings) + { + if (!dirtySettings.Contains(setting)) + dirtySettings.Add(setting); + } }; } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 0cecbb225f..64b1f2d7bc 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -31,7 +31,7 @@ namespace osu.Game.Configuration Set(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); - Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2, 1); + Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); // Online settings Set(OsuSetting.Username, string.Empty); @@ -58,8 +58,8 @@ namespace osu.Game.Configuration Set(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); // Input - Set(OsuSetting.MenuCursorSize, 1.0, 0.5f, 2, 0.01); - Set(OsuSetting.GameplayCursorSize, 1.0, 0.1f, 2, 0.01); + Set(OsuSetting.MenuCursorSize, 1.0f, 0.5f, 2f, 0.01f); + Set(OsuSetting.GameplayCursorSize, 1.0f, 0.1f, 2f, 0.01f); Set(OsuSetting.AutoCursorSize, false); Set(OsuSetting.MouseDisableButtons, false); @@ -69,6 +69,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ShowFpsDisplay, false); Set(OsuSetting.ShowStoryboard, true); + Set(OsuSetting.ShowVideoBackground, true); Set(OsuSetting.BeatmapSkins, true); Set(OsuSetting.BeatmapHitsounds, true); @@ -80,6 +81,8 @@ namespace osu.Game.Configuration Set(OsuSetting.DimLevel, 0.3, 0, 1, 0.01); Set(OsuSetting.BlurLevel, 0, 0, 1, 0.01); + Set(OsuSetting.HitLighting, true); + Set(OsuSetting.ShowInterface, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); Set(OsuSetting.KeyOverlay, false); @@ -111,6 +114,8 @@ namespace osu.Game.Configuration Set(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f); + Set(OsuSetting.UIHoldActivationDelay, 200, 0, 500); + Set(OsuSetting.IntroSequence, IntroSequence.Triangles); } @@ -136,6 +141,7 @@ namespace osu.Game.Configuration DimLevel, BlurLevel, ShowStoryboard, + ShowVideoBackground, KeyOverlay, ScoreMeter, FloatingComments, @@ -178,6 +184,8 @@ namespace osu.Game.Configuration ScalingSizeX, ScalingSizeY, UIScale, - IntroSequence + IntroSequence, + UIHoldActivationDelay, + HitLighting } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 52d3f013ce..17d1bd822e 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -7,10 +7,12 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Humanizer; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework; using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.IO.File; using osu.Framework.Logging; using osu.Framework.Platform; @@ -109,7 +111,7 @@ namespace osu.Game.Database protected async Task Import(ProgressNotification notification, params string[] paths) { notification.Progress = 0; - notification.Text = "Import is initialising..."; + notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising..."; int current = 0; @@ -145,7 +147,7 @@ namespace osu.Game.Database if (imported.Count == 0) { - notification.Text = "Import failed!"; + notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!"; notification.State = ProgressNotificationState.Cancelled; } else @@ -481,12 +483,16 @@ namespace osu.Game.Database { var fileInfos = new List(); + string prefix = reader.Filenames.GetCommonPrefix(); + if (!(prefix.EndsWith("/") || prefix.EndsWith("\\"))) + prefix = string.Empty; + // import files to manager foreach (string file in reader.Filenames) using (Stream s = reader.GetStream(file)) fileInfos.Add(new TFileModel { - Filename = FileSafety.PathStandardise(file), + Filename = FileSafety.PathStandardise(file.Substring(prefix.Length)), FileInfo = files.Add(s) }); @@ -585,7 +591,7 @@ namespace osu.Game.Database /// /// The existing model. /// The newly imported model. - /// Whether the existing model should be restored and used. Returning false will delete the existing a force a re-import. + /// Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import. protected virtual bool CanUndelete(TModel existing, TModel import) => true; private DbSet queryModel() => ContextFactory.Get().Set(); diff --git a/osu.Game/Graphics/Backgrounds/Background.cs b/osu.Game/Graphics/Backgrounds/Background.cs index d13475189d..0f923c3a28 100644 --- a/osu.Game/Graphics/Backgrounds/Background.cs +++ b/osu.Game/Graphics/Backgrounds/Background.cs @@ -57,8 +57,9 @@ namespace osu.Game.Graphics.Backgrounds AddInternal(bufferedContainer = new BufferedContainer { - CacheDrawnFrameBuffer = true, RelativeSizeAxes = Axes.Both, + CacheDrawnFrameBuffer = true, + RedrawOnScale = false, Child = Sprite }); } diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs index 773265d19b..5d549ba217 100644 --- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; namespace osu.Game.Graphics.Containers { @@ -12,12 +14,13 @@ namespace osu.Game.Graphics.Containers { public Action Action; - private const int default_activation_delay = 200; private const int fadeout_delay = 200; - private readonly double activationDelay; + /// + /// Whether currently in a fired state (and the confirm has been sent). + /// + public bool Fired { get; private set; } - private bool fired; private bool confirming; /// @@ -27,35 +30,35 @@ namespace osu.Game.Graphics.Containers public Bindable Progress = new BindableDouble(); - /// - /// Create a new instance. - /// - /// The time requried before an action is confirmed. - protected HoldToConfirmContainer(double activationDelay = default_activation_delay) + private Bindable holdActivationDelay; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) { - this.activationDelay = activationDelay; + holdActivationDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); } protected void BeginConfirm() { - if (confirming || (!AllowMultipleFires && fired)) return; + if (confirming || (!AllowMultipleFires && Fired)) return; confirming = true; - this.TransformBindableTo(Progress, 1, activationDelay * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm()); + this.TransformBindableTo(Progress, 1, holdActivationDelay.Value * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm()); } protected virtual void Confirm() { Action?.Invoke(); - fired = true; + Fired = true; } protected void AbortConfirm() { - if (!AllowMultipleFires && fired) return; + if (!AllowMultipleFires && Fired) return; confirming = false; + Fired = false; this.TransformBindableTo(Progress, 0, fadeout_delay, Easing.Out); } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 9c948d6f90..b117d71006 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -21,8 +21,6 @@ namespace osu.Game.Graphics.Containers private SampleChannel samplePopIn; private SampleChannel samplePopOut; - protected virtual bool PlaySamplesOnStateChange => true; - protected override bool BlockNonPositionalInput => true; /// @@ -32,7 +30,7 @@ namespace osu.Game.Graphics.Containers protected virtual bool DimMainContent => true; [Resolved(CanBeNull = true)] - private OsuGame osuGame { get; set; } + private OsuGame game { get; set; } [Resolved] private PreviewTrackManager previewTrackManager { get; set; } @@ -42,13 +40,22 @@ namespace osu.Game.Graphics.Containers [BackgroundDependencyLoader(true)] private void load(AudioManager audio) { - if (osuGame != null) - OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); - samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in"); samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out"); + } - State.ValueChanged += onStateChanged; + protected override void LoadComplete() + { + if (game != null) + OverlayActivationMode.BindTo(game.OverlayActivationMode); + + OverlayActivationMode.BindValueChanged(mode => + { + if (mode.NewValue == OverlayActivation.Disabled) + State.Value = Visibility.Hidden; + }, true); + + base.LoadComplete(); } /// @@ -62,21 +69,31 @@ namespace osu.Game.Graphics.Containers protected override bool OnClick(ClickEvent e) { - closeIfOutside(e); + if (!base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) + Hide(); return base.OnClick(e); } - protected override bool OnDragEnd(DragEndEvent e) - { - closeIfOutside(e); - return base.OnDragEnd(e); - } + private bool closeOnDragEnd; - private void closeIfOutside(MouseEvent e) + protected override bool OnDragStart(DragStartEvent e) { if (!base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) + closeOnDragEnd = true; + + return base.OnDragStart(e); + } + + protected override bool OnDragEnd(DragEndEvent e) + { + if (closeOnDragEnd) + { Hide(); + closeOnDragEnd = false; + } + + return base.OnDragEnd(e); } public virtual bool OnPressed(GlobalAction action) @@ -96,26 +113,28 @@ namespace osu.Game.Graphics.Containers public bool OnReleased(GlobalAction action) => false; - private void onStateChanged(ValueChangedEvent state) + protected override void UpdateState(ValueChangedEvent state) { switch (state.NewValue) { case Visibility.Visible: - if (OverlayActivationMode.Value != OverlayActivation.Disabled) + if (OverlayActivationMode.Value == OverlayActivation.Disabled) { - if (PlaySamplesOnStateChange) samplePopIn?.Play(); - if (BlockScreenWideMouse && DimMainContent) osuGame?.AddBlockingOverlay(this); + State.Value = Visibility.Hidden; + return; } - else - Hide(); + samplePopIn?.Play(); + if (BlockScreenWideMouse && DimMainContent) game?.AddBlockingOverlay(this); break; case Visibility.Hidden: - if (PlaySamplesOnStateChange) samplePopOut?.Play(); - if (BlockScreenWideMouse) osuGame?.RemoveBlockingOverlay(this); + samplePopOut?.Play(); + if (BlockScreenWideMouse) game?.RemoveBlockingOverlay(this); break; } + + base.UpdateState(state); } protected override void PopOut() @@ -127,7 +146,7 @@ namespace osu.Game.Graphics.Containers protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - osuGame?.RemoveBlockingOverlay(this); + game?.RemoveBlockingOverlay(this); } } } diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index 2b7635cc88..7683bbcd63 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -35,6 +35,8 @@ namespace osu.Game.Graphics.Containers protected Bindable ShowStoryboard { get; private set; } + protected Bindable ShowVideo { get; private set; } + protected double DimLevel => EnableUserDim.Value ? UserDimLevel.Value : 0; protected override Container Content => dimContent; @@ -54,10 +56,12 @@ namespace osu.Game.Graphics.Containers { UserDimLevel = config.GetBindable(OsuSetting.DimLevel); ShowStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); + ShowVideo = config.GetBindable(OsuSetting.ShowVideoBackground); EnableUserDim.ValueChanged += _ => UpdateVisuals(); UserDimLevel.ValueChanged += _ => UpdateVisuals(); ShowStoryboard.ValueChanged += _ => UpdateVisuals(); + ShowVideo.ValueChanged += _ => UpdateVisuals(); StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals(); } diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index e103798355..5a83d8e4ce 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -124,7 +124,7 @@ namespace osu.Game.Graphics.Cursor public class Cursor : Container { private Container cursorContainer; - private Bindable cursorScale; + private Bindable cursorScale; private const float base_scale = 0.15f; public Sprite AdditiveLayer; @@ -159,9 +159,8 @@ namespace osu.Game.Graphics.Cursor } }; - cursorScale = config.GetBindable(OsuSetting.MenuCursorSize); - cursorScale.ValueChanged += scale => cursorContainer.Scale = new Vector2((float)scale.NewValue * base_scale); - cursorScale.TriggerChange(); + cursorScale = config.GetBindable(OsuSetting.MenuCursorSize); + cursorScale.BindValueChanged(scale => cursorContainer.Scale = new Vector2(scale.NewValue * base_scale), true); } } diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index f532302de2..02d928ec66 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -83,11 +83,19 @@ namespace osu.Game.Graphics const int frames_to_wait = 3; int framesWaited = 0; - ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() => framesWaited++, 0, true); - while (framesWaited < frames_to_wait) - Thread.Sleep(10); - waitDelegate.Cancel(); + using (var framesWaitedEvent = new ManualResetEventSlim(false)) + { + ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() => + { + if (framesWaited++ < frames_to_wait) + // ReSharper disable once AccessToDisposedClosure + framesWaitedEvent.Set(); + }, 10, true); + + framesWaitedEvent.Wait(); + waitDelegate.Cancel(); + } } using (var image = await host.TakeScreenshotAsync()) diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index 24816deeb5..12688da9df 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -55,6 +55,7 @@ namespace osu.Game.Graphics.Sprites Origin = Anchor.Centre, BlurSigma = new Vector2(4), CacheDrawnFrameBuffer = true, + RedrawOnScale = false, RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, Size = new Vector2(3f), diff --git a/osu.Game/Graphics/UserInterface/GradientLineTabControl.cs b/osu.Game/Graphics/UserInterface/GradientLineTabControl.cs new file mode 100644 index 0000000000..baca57ea89 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/GradientLineTabControl.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; +using osu.Framework.Graphics.Colour; + +namespace osu.Game.Graphics.UserInterface +{ + public abstract class GradientLineTabControl : PageTabControl + { + protected Color4 LineColour + { + get => line.Colour; + set => line.Colour = value; + } + + private readonly GradientLine line; + + protected GradientLineTabControl() + { + RelativeSizeAxes = Axes.X; + + AddInternal(line = new GradientLine + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }); + } + + protected override Dropdown CreateDropdown() => null; + + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20, 0), + }; + + private class GradientLine : GridContainer + { + public GradientLine() + { + RelativeSizeAxes = Axes.X; + Size = new Vector2(0.8f, 1.5f); + + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(mode: GridSizeMode.Relative, size: 0.4f), + new Dimension(), + }; + + Content = new[] + { + new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(Color4.Transparent, Color4.White) + }, + new Box + { + RelativeSizeAxes = Axes.Both, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Transparent) + }, + } + }; + } + } + } +} diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs index 1fb73efa65..4f678b7218 100644 --- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -34,12 +34,12 @@ namespace osu.Game.Graphics.UserInterface this.buttons = buttons ?? new[] { MouseButton.Left }; } - protected override bool OnMouseUp(MouseUpEvent e) + protected override bool OnClick(ClickEvent e) { if (buttons.Contains(e.Button) && Contains(e.ScreenSpaceMousePosition)) sampleClick?.Play(); - return base.OnMouseUp(e); + return base.OnClick(e); } [BackgroundDependencyLoader] diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index a0d3745180..ddcb626701 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -63,7 +63,7 @@ namespace osu.Game.Graphics.UserInterface Margin = new MarginPadding { Top = 8, Bottom = 8 }, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - Text = (value as Enum)?.GetDescription() ?? value.ToString(), + Text = CreateText(), Font = OsuFont.GetFont(size: 14) }, box = new Box @@ -81,6 +81,8 @@ namespace osu.Game.Graphics.UserInterface Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } + protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString(); + protected override bool OnHover(HoverEvent e) { if (!Active.Value) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs new file mode 100644 index 0000000000..2e659825b7 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs @@ -0,0 +1,132 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public abstract class LabelledComponent : CompositeDrawable + where T : Drawable + { + protected const float CONTENT_PADDING_VERTICAL = 10; + protected const float CONTENT_PADDING_HORIZONTAL = 15; + protected const float CORNER_RADIUS = 15; + + /// + /// The component that is being displayed. + /// + protected readonly T Component; + + private readonly OsuTextFlowContainer labelText; + private readonly OsuTextFlowContainer descriptionText; + + /// + /// Creates a new . + /// + /// Whether the component should be padded or should be expanded to the bounds of this . + protected LabelledComponent(bool padded) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + CornerRadius = CORNER_RADIUS; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("1c2125"), + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = padded + ? new MarginPadding { Horizontal = CONTENT_PADDING_HORIZONTAL, Vertical = CONTENT_PADDING_VERTICAL } + : new MarginPadding { Left = CONTENT_PADDING_HORIZONTAL }, + Spacing = new Vector2(0, 12), + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] + { + new Drawable[] + { + labelText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Bold)) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 20 } + }, + new Container + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = Component = CreateComponent().With(d => + { + d.Anchor = Anchor.CentreRight; + d.Origin = Anchor.CentreRight; + }) + } + }, + }, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }, + descriptionText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold, italics: true)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Bottom = padded ? 0 : CONTENT_PADDING_VERTICAL }, + Alpha = 0, + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour osuColour) + { + descriptionText.Colour = osuColour.Yellow; + } + + public string Label + { + set => labelText.Text = value; + } + + public string Description + { + set + { + descriptionText.Text = value; + + if (!string.IsNullOrEmpty(value)) + descriptionText.Show(); + else + descriptionText.Hide(); + } + } + + /// + /// Creates the component that should be displayed. + /// + /// The component. + protected abstract T CreateComponent(); + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs new file mode 100644 index 0000000000..50d2a14482 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class LabelledTextBox : LabelledComponent + { + public event TextBox.OnCommitHandler OnCommit; + + public LabelledTextBox() + : base(false) + { + } + + public bool ReadOnly + { + set => Component.ReadOnly = value; + } + + public string PlaceholderText + { + set => Component.PlaceholderText = value; + } + + public string Text + { + set => Component.Text = value; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Component.BorderColour = colours.Blue; + } + + protected override OsuTextBox CreateComponent() => new OsuTextBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + CornerRadius = CORNER_RADIUS, + }.With(t => t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText)); + } +} diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index 370d6786f5..bf4e881ed0 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -50,7 +50,16 @@ namespace osu.Game.IO string path = info.StoragePath; // we may be re-adding a file to fix missing store entries. - if (!Storage.Exists(path)) + bool requiresCopy = !Storage.Exists(path); + + if (!requiresCopy) + { + // even if the file already exists, check the existing checksum for safety. + using (var stream = Storage.GetStream(path)) + requiresCopy |= stream.ComputeSHA2Hash() != hash; + } + + if (requiresCopy) { data.Seek(0, SeekOrigin.Begin); diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs new file mode 100644 index 0000000000..826233a2b0 --- /dev/null +++ b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20190913104727_AddBeatmapVideo")] + partial class AddBeatmapVideo + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.Property("VideoFile"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs new file mode 100644 index 0000000000..9ed0943acd --- /dev/null +++ b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddBeatmapVideo : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "VideoFile", + table: "BeatmapMetadata", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "VideoFile", + table: "BeatmapMetadata"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index 761dca2801..a6d9d1f3cb 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace osu.Game.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.2.4-servicing-10062"); + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => { @@ -139,6 +139,8 @@ namespace osu.Game.Migrations b.Property("TitleUnicode"); + b.Property("VideoFile"); + b.HasKey("ID"); b.ToTable("BeatmapMetadata"); diff --git a/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs index 140e228acd..f949ab5da5 100644 --- a/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs +++ b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs @@ -24,7 +24,7 @@ namespace osu.Game.Online.API.Requests.Responses public string Url { get; set; } [JsonProperty("type")] - public string Type { get; set; } + public ChangelogEntryType Type { get; set; } [JsonProperty("category")] public string Category { get; set; } @@ -44,4 +44,10 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("github_user")] public APIChangelogUser GithubUser { get; set; } } + + public enum ChangelogEntryType + { + Add, + Fix + } } diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index db26945ef3..24d17612ee 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; namespace osu.Game.Online.Chat @@ -10,16 +11,16 @@ namespace osu.Game.Online.Chat public static class MessageFormatter { // [[Performance Points]] -> wiki:Performance Points (https://osu.ppy.sh/wiki/Performance_Points) - private static readonly Regex wiki_regex = new Regex(@"\[\[([^\]]+)\]\]"); + private static readonly Regex wiki_regex = new Regex(@"\[\[(?[^\]]+)\]\]"); // (test)[https://osu.ppy.sh/b/1234] -> test (https://osu.ppy.sh/b/1234) - private static readonly Regex old_link_regex = new Regex(@"\(([^\)]*)\)\[([a-z]+://[^ ]+)\]"); + private static readonly Regex old_link_regex = new Regex(@"\((?(((?<=\\)[\(\)])|[^\(\)])*(((?\()(((?<=\\)[\(\)])|[^\(\)])*)+((?\))(((?<=\\)[\(\)])|[^\(\)])*)+)*(?(open)(?!)))\)\[(?[a-z]+://[^ ]+)\]"); // [https://osu.ppy.sh/b/1234 Beatmap [Hard] (poop)] -> Beatmap [hard] (poop) (https://osu.ppy.sh/b/1234) - private static readonly Regex new_link_regex = new Regex(@"\[([a-z]+://[^ ]+) ([^\[\]]*(((?\[)[^\[\]]*)+((?\])[^\[\]]*)+)*(?(open)(?!)))\]"); + private static readonly Regex new_link_regex = new Regex(@"\[(?[a-z]+://[^ ]+) (?(((?<=\\)[\[\]])|[^\[\]])*(((?\[)(((?<=\\)[\[\]])|[^\[\]])*)+((?\])(((?<=\\)[\[\]])|[^\[\]])*)+)*(?(open)(?!)))\]"); // [test](https://osu.ppy.sh/b/1234) -> test (https://osu.ppy.sh/b/1234) aka correct markdown format - private static readonly Regex markdown_link_regex = new Regex(@"\[([^\]]*)\]\(([a-z]+://[^ ]+)\)"); + private static readonly Regex markdown_link_regex = new Regex(@"\[(?(((?<=\\)[\[\]])|[^\[\]])*(((?\[)(((?<=\\)[\[\]])|[^\[\]])*)+((?\])(((?<=\\)[\[\]])|[^\[\]])*)+)*(?(open)(?!)))\]\((?[a-z]+://[^ ]+)\)"); // advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used // This is in the format (, [optional]): @@ -48,7 +49,7 @@ namespace osu.Game.Online.Chat // Unicode emojis private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])"); - private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null) + private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null, char[] escapeChars = null) { int captureOffset = 0; @@ -58,16 +59,20 @@ namespace osu.Game.Online.Chat var displayText = string.Format(display, m.Groups[0], - m.Groups.Count > 1 ? m.Groups[1].Value : "", - m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim(); + m.Groups["text"].Value, + m.Groups["url"].Value).Trim(); var linkText = string.Format(link, m.Groups[0], - m.Groups.Count > 1 ? m.Groups[1].Value : "", - m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim(); + m.Groups["text"].Value, + m.Groups["url"].Value).Trim(); if (displayText.Length == 0 || linkText.Length == 0) continue; + // Remove backslash escapes in front of the characters provided in escapeChars + if (escapeChars != null) + displayText = escapeChars.Aggregate(displayText, (current, c) => current.Replace($"\\{c}", c.ToString())); + // Check for encapsulated links if (result.Links.Find(l => (l.Index <= index && l.Index + l.Length >= index + m.Length) || (index <= l.Index && index + m.Length >= l.Index + l.Length)) == null) { @@ -183,13 +188,13 @@ namespace osu.Game.Online.Chat var result = new MessageFormatterResult(toFormat); // handle the [link display] format - handleMatches(new_link_regex, "{2}", "{1}", result, startIndex); + handleMatches(new_link_regex, "{1}", "{2}", result, startIndex, escapeChars: new[] { '[', ']' }); // handle the standard markdown []() format - handleMatches(markdown_link_regex, "{1}", "{2}", result, startIndex); + handleMatches(markdown_link_regex, "{1}", "{2}", result, startIndex, escapeChars: new[] { '[', ']' }); // handle the ()[] link format - handleMatches(old_link_regex, "{1}", "{2}", result, startIndex); + handleMatches(old_link_regex, "{1}", "{2}", result, startIndex, escapeChars: new[] { '(', ')' }); // handle wiki links handleMatches(wiki_regex, "{1}", "https://osu.ppy.sh/wiki/{1}", result, startIndex); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 98f15599fc..83de0635fb 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -35,6 +35,10 @@ namespace osu.Game.Online.Leaderboards private bool scoresLoadedOnce; + private readonly Container content; + + protected override Container Content => content; + private IEnumerable scores; public IEnumerable Scores @@ -60,13 +64,13 @@ namespace osu.Game.Online.Leaderboards // ensure placeholder is hidden when displaying scores PlaceholderState = PlaceholderState.Successful; - var sf = CreateScoreFlow(); - sf.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); + var scoreFlow = CreateScoreFlow(); + scoreFlow.ChildrenEnumerable = scores.Select((s, index) => CreateDrawableScore(s, index + 1)); // schedule because we may not be loaded yet (LoadComponentAsync complains). - showScoresDelegate = Schedule(() => LoadComponentAsync(sf, _ => + showScoresDelegate = Schedule(() => LoadComponentAsync(scoreFlow, _ => { - scrollContainer.Add(scrollFlow = sf); + scrollContainer.Add(scrollFlow = scoreFlow); int i = 0; @@ -116,9 +120,7 @@ namespace osu.Game.Online.Leaderboards { if (value != PlaceholderState.Successful) { - getScoresRequest?.Cancel(); - getScoresRequest = null; - Scores = null; + Reset(); } if (value == placeholderState) @@ -133,6 +135,10 @@ namespace osu.Game.Online.Leaderboards }); break; + case PlaceholderState.NoneSelected: + replacePlaceholder(new MessagePlaceholder(@"Please select a beatmap!")); + break; + case PlaceholderState.Unavailable: replacePlaceholder(new MessagePlaceholder(@"Leaderboards are not available for this beatmap!")); break; @@ -158,12 +164,35 @@ namespace osu.Game.Online.Leaderboards protected Leaderboard() { - Children = new Drawable[] + InternalChildren = new Drawable[] { - scrollContainer = new OsuScrollContainer + new GridContainer { RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + scrollContainer = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + } + }, + new Drawable[] + { + content = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }, + } + }, }, loading = new LoadingAnimation(), placeholderContainer = new Container @@ -173,6 +202,13 @@ namespace osu.Game.Online.Leaderboards }; } + protected virtual void Reset() + { + getScoresRequest?.Cancel(); + getScoresRequest = null; + Scores = null; + } + private IAPIProvider api; private ScheduledDelegate pendingUpdateScores; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 008f8208eb..9387482f14 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -20,23 +20,23 @@ using osu.Game.Scoring; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; +using Humanizer; namespace osu.Game.Online.Leaderboards { public class LeaderboardScore : OsuClickableContainer { - public readonly int RankPosition; - public const float HEIGHT = 60; private const float corner_radius = 5; private const float edge_margin = 5; private const float background_alpha = 0.25f; - private const float rank_width = 30; + private const float rank_width = 35; protected Container RankContainer { get; private set; } private readonly ScoreInfo score; + private readonly int rank; private Box background; private Container content; @@ -52,7 +52,7 @@ namespace osu.Game.Online.Leaderboards public LeaderboardScore(ScoreInfo score, int rank) { this.score = score; - RankPosition = rank; + this.rank = rank; RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -79,8 +79,8 @@ namespace osu.Game.Online.Leaderboards { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 22, italics: true), - Text = RankPosition.ToString(), + Font = OsuFont.GetFont(size: 20, italics: true), + Text = rank.ToMetric(decimals: rank < 100000 ? 1 : 0), }, }, }, @@ -215,6 +215,7 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.BottomRight, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + Spacing = new Vector2(1), ChildrenEnumerable = score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) }, }, diff --git a/osu.Game/Online/Leaderboards/PlaceholderState.cs b/osu.Game/Online/Leaderboards/PlaceholderState.cs index 930e1df484..297241fa73 100644 --- a/osu.Game/Online/Leaderboards/PlaceholderState.cs +++ b/osu.Game/Online/Leaderboards/PlaceholderState.cs @@ -9,6 +9,7 @@ namespace osu.Game.Online.Leaderboards Retrieving, NetworkFailure, Unavailable, + NoneSelected, NoScores, NotLoggedIn, NotSupporter, diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index de8f316b06..d6b8ad3e67 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -207,6 +207,7 @@ namespace osu.Game FileStore.Cleanup(); AddInternal(API); + AddInternal(RulesetConfigCache); GlobalActionContainer globalBinding; diff --git a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs index dcd58db427..e2a725ec46 100644 --- a/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs +++ b/osu.Game/Overlays/BeatmapSet/LeaderboardScopeSelector.cs @@ -1,60 +1,37 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics.UserInterface; using osu.Game.Screens.Select.Leaderboards; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osuTK; using osu.Game.Graphics.UserInterface; -using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Framework.Allocation; using osuTK.Graphics; -using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Graphics; namespace osu.Game.Overlays.BeatmapSet { - public class LeaderboardScopeSelector : PageTabControl + public class LeaderboardScopeSelector : GradientLineTabControl { protected override bool AddEnumEntriesAutomatically => false; - protected override Dropdown CreateDropdown() => null; - protected override TabItem CreateTabItem(BeatmapLeaderboardScope value) => new ScopeSelectorTabItem(value); public LeaderboardScopeSelector() { - RelativeSizeAxes = Axes.X; - AddItem(BeatmapLeaderboardScope.Global); AddItem(BeatmapLeaderboardScope.Country); AddItem(BeatmapLeaderboardScope.Friend); - - AddInternal(new GradientLine - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - }); } [BackgroundDependencyLoader] private void load(OsuColour colours) { AccentColour = colours.Blue; + LineColour = Color4.Gray; } - protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(20, 0), - }; - private class ScopeSelectorTabItem : PageTabItem { public ScopeSelectorTabItem(BeatmapLeaderboardScope value) @@ -77,43 +54,5 @@ namespace osu.Game.Overlays.BeatmapSet Text.FadeColour(Color4.White); } } - - private class GradientLine : GridContainer - { - public GradientLine() - { - RelativeSizeAxes = Axes.X; - Size = new Vector2(0.8f, 1.5f); - - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(mode: GridSizeMode.Relative, size: 0.4f), - new Dimension(), - }; - - Content = new[] - { - new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(Color4.Transparent, Color4.Gray), - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Gray, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(Color4.Gray, Color4.Transparent), - }, - } - }; - } - } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 347522fb48..58f5f02956 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -171,6 +171,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, + Spacing = new Vector2(1), ChildrenEnumerable = score.Mods.Select(m => new ModIcon(m) { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 6761d0f710..b9664d7c2f 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -172,7 +172,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores : this(new FillFlowContainer { AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1), }) { } diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 3d145af562..bce1be5941 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -14,6 +14,8 @@ using osu.Game.Graphics.Sprites; using osu.Game.Users; using osuTK.Graphics; using osu.Framework.Allocation; +using System.Net; +using osuTK; namespace osu.Game.Overlays.Changelog { @@ -66,22 +68,34 @@ namespace osu.Game.Overlays.Changelog foreach (APIChangelogEntry entry in categoryEntries) { - LinkFlowContainer title = new LinkFlowContainer - { - Direction = FillDirection.Full, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Vertical = 5 }, - }; - var entryColour = entry.Major ? colours.YellowLight : Color4.White; - title.AddIcon(FontAwesome.Solid.Check, t => + LinkFlowContainer title; + + Container titleContainer = new Container { - t.Font = fontSmall; - t.Colour = entryColour; - t.Padding = new MarginPadding { Left = -17, Right = 5 }; - }); + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Vertical = 5 }, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Size = new Vector2(fontSmall.Size), + Icon = entry.Type == ChangelogEntryType.Fix ? FontAwesome.Solid.Check : FontAwesome.Solid.Plus, + Colour = entryColour, + Margin = new MarginPadding { Right = 5 }, + }, + title = new LinkFlowContainer + { + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + } + }; title.AddText(entry.Title, t => { @@ -138,7 +152,7 @@ namespace osu.Game.Overlays.Changelog t.Colour = entryColour; }); - ChangelogEntries.Add(title); + ChangelogEntries.Add(titleContainer); if (!string.IsNullOrEmpty(entry.MessageHtml)) { @@ -149,7 +163,7 @@ namespace osu.Game.Overlays.Changelog }; // todo: use markdown parsing once API returns markdown - message.AddText(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty), t => + message.AddText(WebUtility.HtmlDecode(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty)), t => { t.Font = fontSmall; t.Colour = new Color4(235, 184, 254, 255); diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 7755c8a6a6..dfe3669813 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -170,7 +170,7 @@ namespace osu.Game.Overlays var tcs = new TaskCompletionSource(); var req = new GetChangelogRequest(); - req.Success += res => + req.Success += res => Schedule(() => { // remap streams to builds to ensure model equality res.Builds.ForEach(b => b.UpdateStream = res.Streams.Find(s => s.Id == b.UpdateStream.Id)); @@ -182,7 +182,7 @@ namespace osu.Game.Overlays header.Streams.Populate(res.Streams); tcs.SetResult(true); - }; + }); req.Failure += _ => initialFetchTask = null; req.Perform(API); diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 2576b38ec8..7596231a3d 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -31,6 +31,8 @@ namespace osu.Game.Overlays.Chat protected virtual float MessagePadding => default_message_padding; + private const float timestamp_padding = 65; + private const float default_horizontal_padding = 15; protected virtual float HorizontalPadding => default_horizontal_padding; @@ -87,7 +89,12 @@ namespace osu.Game.Overlays.Chat { Shadow = false, Colour = hasBackground ? customUsernameColour : username_colours[message.Sender.Id % username_colours.Length], - Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true) + Truncate = true, + EllipsisString = "… :", + Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + MaxWidth = default_message_padding - timestamp_padding }; if (hasBackground) @@ -142,6 +149,7 @@ namespace osu.Game.Overlays.Chat new MessageSender(message.Sender) { AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = timestamp_padding }, Origin = Anchor.TopRight, Anchor = Anchor.TopRight, Child = effectedUsername, diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 53a05656b1..6f848c7627 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays private Box chatBackground; private Box tabBackground; - public Bindable ChatHeight { get; set; } + public Bindable ChatHeight { get; set; } private Container channelSelectionContainer; protected ChannelSelectionOverlay ChannelSelectionOverlay; @@ -190,14 +190,13 @@ namespace osu.Game.Overlays ChannelSelectionOverlay.OnRequestJoin = channel => channelManager.JoinChannel(channel); ChannelSelectionOverlay.OnRequestLeave = channelManager.LeaveChannel; - ChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); - ChatHeight.ValueChanged += height => + ChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); + ChatHeight.BindValueChanged(height => { - chatContainer.Height = (float)height.NewValue; - channelSelectionContainer.Height = 1f - (float)height.NewValue; - tabBackground.FadeTo(height.NewValue == 1 ? 1 : 0.8f, 200); - }; - ChatHeight.TriggerChange(); + chatContainer.Height = height.NewValue; + channelSelectionContainer.Height = 1f - height.NewValue; + tabBackground.FadeTo(height.NewValue == 1f ? 1f : 0.8f, 200); + }, true); chatBackground.Colour = colours.ChatBlue; @@ -273,7 +272,7 @@ namespace osu.Game.Overlays } } - private double startDragChatHeight; + private float startDragChatHeight; private bool isDragging; protected override bool OnDragStart(DragStartEvent e) @@ -291,7 +290,7 @@ namespace osu.Game.Overlays { if (isDragging) { - double targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; + float targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; // If the channel selection screen is shown, mind its minimum height if (ChannelSelectionOverlay.State.Value == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 1022edfe81..cff887865a 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -13,20 +13,17 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; -using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Overlays.Dialog { - public class PopupDialog : OsuFocusedOverlayContainer + public abstract class PopupDialog : VisibilityContainer { public static readonly float ENTER_DURATION = 500; public static readonly float EXIT_DURATION = 200; - protected override bool BlockPositionalInput => false; - private readonly Vector2 ringSize = new Vector2(100f); private readonly Vector2 ringMinifiedSize = new Vector2(20f); private readonly Vector2 buttonsEnterSpacing = new Vector2(0f, 50f); @@ -90,7 +87,7 @@ namespace osu.Game.Overlays.Dialog } } - public PopupDialog() + protected PopupDialog() { RelativeSizeAxes = Axes.Both; @@ -202,18 +199,6 @@ namespace osu.Game.Overlays.Dialog }; } - public override bool OnPressed(GlobalAction action) - { - switch (action) - { - case GlobalAction.Select: - Buttons.OfType().FirstOrDefault()?.Click(); - return true; - } - - return base.OnPressed(action); - } - protected override bool OnKeyDown(KeyDownEvent e) { if (e.Repeat) return false; @@ -238,8 +223,6 @@ namespace osu.Game.Overlays.Dialog protected override void PopIn() { - base.PopIn(); - actionInvoked = false; // Reset various animations but only if the dialog animation fully completed @@ -263,7 +246,6 @@ namespace osu.Game.Overlays.Dialog // This is presumed to always be a sane default "cancel" action. buttonsContainer.Last().Click(); - base.PopOut(); content.FadeOut(EXIT_DURATION, Easing.InSine); } diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index aaae7bcf5c..59d748bc5d 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -5,13 +5,16 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Dialog; using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; +using System.Linq; namespace osu.Game.Overlays { public class DialogOverlay : OsuFocusedOverlayContainer { private readonly Container dialogContainer; - private PopupDialog currentDialog; + + public PopupDialog CurrentDialog { get; private set; } public DialogOverlay() { @@ -29,20 +32,18 @@ namespace osu.Game.Overlays public void Push(PopupDialog dialog) { - if (dialog == currentDialog) return; + if (dialog == CurrentDialog) return; - currentDialog?.Hide(); - currentDialog = dialog; + CurrentDialog?.Hide(); + CurrentDialog = dialog; - dialogContainer.Add(currentDialog); + dialogContainer.Add(CurrentDialog); - currentDialog.Show(); - currentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue); + CurrentDialog.Show(); + CurrentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue); Show(); } - protected override bool PlaySamplesOnStateChange => false; - protected override bool BlockNonPositionalInput => true; private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v) @@ -52,8 +53,11 @@ namespace osu.Game.Overlays //handle the dialog being dismissed. dialog.Delay(PopupDialog.EXIT_DURATION).Expire(); - if (dialog == currentDialog) + if (dialog == CurrentDialog) + { Hide(); + CurrentDialog = null; + } } protected override void PopIn() @@ -66,13 +70,25 @@ namespace osu.Game.Overlays { base.PopOut(); - if (currentDialog?.State.Value == Visibility.Visible) + if (CurrentDialog?.State.Value == Visibility.Visible) { - currentDialog.Hide(); + CurrentDialog.Hide(); return; } this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine); } + + public override bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Select: + CurrentDialog?.Buttons.OfType().FirstOrDefault()?.Click(); + return true; + } + + return base.OnPressed(action); + } } } diff --git a/osu.Game/Overlays/HoldToConfirmOverlay.cs b/osu.Game/Overlays/HoldToConfirmOverlay.cs index fdc6f096bc..eb325d8dd3 100644 --- a/osu.Game/Overlays/HoldToConfirmOverlay.cs +++ b/osu.Game/Overlays/HoldToConfirmOverlay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays protected override void Dispose(bool isDisposing) { - audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, audioVolume); + audio?.Tracks.RemoveAdjustment(AdjustableProperty.Volume, audioVolume); base.Dispose(isDisposing); } } diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 539601c359..5b528c5ab2 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -18,7 +19,6 @@ namespace osu.Game.Overlays.Music public class PlaylistList : CompositeDrawable { public Action Selected; - public Action OrderChanged; private readonly ItemsScrollContainer items; @@ -28,7 +28,6 @@ namespace osu.Game.Overlays.Music { RelativeSizeAxes = Axes.Both, Selected = set => Selected?.Invoke(set), - OrderChanged = (s, i) => OrderChanged?.Invoke(s, i) }; } @@ -45,13 +44,17 @@ namespace osu.Game.Overlays.Music private class ItemsScrollContainer : OsuScrollContainer { public Action Selected; - public Action OrderChanged; private readonly SearchContainer search; private readonly FillFlowContainer items; private readonly IBindable beatmapBacking = new Bindable(); + private IBindableList beatmaps; + + [Resolved] + private MusicController musicController { get; set; } + public ItemsScrollContainer() { Children = new Drawable[] @@ -73,27 +76,35 @@ namespace osu.Game.Overlays.Music } [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, IBindable beatmap) + private void load(IBindable beatmap) { - beatmaps.GetAllUsableBeatmapSets().ForEach(addBeatmapSet); - beatmaps.ItemAdded += addBeatmapSet; - beatmaps.ItemRemoved += removeBeatmapSet; + beatmaps = musicController.BeatmapSets.GetBoundCopy(); + beatmaps.ItemsAdded += i => i.ForEach(addBeatmapSet); + beatmaps.ItemsRemoved += i => i.ForEach(removeBeatmapSet); + beatmaps.ForEach(addBeatmapSet); beatmapBacking.BindTo(beatmap); beatmapBacking.ValueChanged += _ => updateSelectedSet(); } - private void addBeatmapSet(BeatmapSetInfo obj) => Schedule(() => + private void addBeatmapSet(BeatmapSetInfo obj) { - items.Insert(items.Count - 1, new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) }); - }); + if (obj == draggedItem?.BeatmapSetInfo) return; - private void removeBeatmapSet(BeatmapSetInfo obj) => Schedule(() => + Schedule(() => items.Insert(items.Count - 1, new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) })); + } + + private void removeBeatmapSet(BeatmapSetInfo obj) { - var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == obj.ID); - if (itemToRemove != null) - items.Remove(itemToRemove); - }); + if (obj == draggedItem?.BeatmapSetInfo) return; + + Schedule(() => + { + var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == obj.ID); + if (itemToRemove != null) + items.Remove(itemToRemove); + }); + } private void updateSelectedSet() { @@ -112,6 +123,8 @@ namespace osu.Game.Overlays.Music private Vector2 nativeDragPosition; private PlaylistItem draggedItem; + private int? dragDestination; + protected override bool OnDragStart(DragStartEvent e) { nativeDragPosition = e.ScreenSpaceMousePosition; @@ -131,10 +144,17 @@ namespace osu.Game.Overlays.Music protected override bool OnDragEnd(DragEndEvent e) { nativeDragPosition = e.ScreenSpaceMousePosition; - var handled = draggedItem != null || base.OnDragEnd(e); - draggedItem = null; - return handled; + if (draggedItem == null) + return base.OnDragEnd(e); + + if (dragDestination != null) + musicController.ChangeBeatmapSetPosition(draggedItem.BeatmapSetInfo, dragDestination.Value); + + draggedItem = null; + dragDestination = null; + + return true; } protected override void Update() @@ -210,7 +230,7 @@ namespace osu.Game.Overlays.Music } items.SetLayoutPosition(draggedItem, dstIndex); - OrderChanged?.Invoke(draggedItem.BeatmapSetInfo, dstIndex); + dragDestination = dstIndex; } private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index ec3d708645..ae81a6c117 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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.Allocation; using osu.Framework.Bindables; @@ -22,12 +21,6 @@ namespace osu.Game.Overlays.Music private const float transition_duration = 600; private const float playlist_height = 510; - /// - /// Invoked when the order of an item in the list has changed. - /// The second parameter indicates the new index of the item. - /// - public Action OrderChanged; - private readonly Bindable beatmap = new Bindable(); private BeatmapManager beatmaps; @@ -65,7 +58,6 @@ namespace osu.Game.Overlays.Music RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = 95, Bottom = 10, Right = 10 }, Selected = itemSelected, - OrderChanged = (s, i) => OrderChanged?.Invoke(s, i) }, filter = new FilterControl { diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 6ad147735b..db94b0278f 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; +using osu.Framework.MathUtils; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; @@ -24,7 +25,9 @@ namespace osu.Game.Overlays [Resolved] private BeatmapManager beatmaps { get; set; } - private List beatmapSets; + public IBindableList BeatmapSets => beatmapSets; + + private readonly BindableList beatmapSets = new BindableList(); public bool IsUserPaused { get; private set; } @@ -46,7 +49,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - beatmapSets = beatmaps.GetAllUsableBeatmapSets(); + beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next())); beatmaps.ItemAdded += handleBeatmapAdded; beatmaps.ItemRemoved += handleBeatmapRemoved; } @@ -140,7 +143,7 @@ namespace osu.Game.Overlays { queuedDirection = TrackChangeDirection.Prev; - var playable = beatmapSets.TakeWhile(i => i.ID != current.BeatmapSetInfo.ID).LastOrDefault() ?? beatmapSets.LastOrDefault(); + var playable = BeatmapSets.TakeWhile(i => i.ID != current.BeatmapSetInfo.ID).LastOrDefault() ?? BeatmapSets.LastOrDefault(); if (playable != null) { @@ -165,7 +168,7 @@ namespace osu.Game.Overlays if (!instant) queuedDirection = TrackChangeDirection.Next; - var playable = beatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).Skip(1).FirstOrDefault() ?? beatmapSets.FirstOrDefault(); + var playable = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).Skip(1).FirstOrDefault() ?? BeatmapSets.FirstOrDefault(); if (playable != null) { @@ -200,8 +203,8 @@ namespace osu.Game.Overlays else { //figure out the best direction based on order in playlist. - var last = beatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count(); - var next = beatmap.NewValue == null ? -1 : beatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count(); + var last = BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count(); + var next = beatmap.NewValue == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count(); direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index a3243a655e..6b79f2af07 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -81,7 +81,6 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.X, Y = player_height + 10, - OrderChanged = musicController.ChangeBeatmapSetPosition }, playerContainer = new Container { @@ -346,10 +345,12 @@ namespace osu.Game.Overlays public Background(WorkingBeatmap beatmap = null) { this.beatmap = beatmap; - CacheDrawnFrameBuffer = true; + Depth = float.MaxValue; RelativeSizeAxes = Axes.Both; + CacheDrawnFrameBuffer = true; + Children = new Drawable[] { sprite = new Sprite diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index e54ce44ca2..6362d3dfb0 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -12,12 +12,13 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Beatmaps; using osu.Framework.Localisation; +using osu.Framework.Graphics.Containers; namespace osu.Game.Overlays.Profile.Sections.Ranks { public abstract class DrawableProfileScore : DrawableProfileRow { - private readonly ScoreModsContainer modsContainer; + private readonly FillFlowContainer modsContainer; protected readonly ScoreInfo Score; protected DrawableProfileScore(ScoreInfo score) @@ -28,12 +29,12 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Height = 60; Children = new Drawable[] { - modsContainer = new ScoreModsContainer + modsContainer = new FillFlowContainer { - AutoSizeAxes = Axes.Y, + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Width = 60, + Spacing = new Vector2(1), Margin = new MarginPadding { Right = 160 } } }; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/ScoreModsContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/ScoreModsContainer.cs deleted file mode 100644 index 1ce04effa8..0000000000 --- a/osu.Game/Overlays/Profile/Sections/Ranks/ScoreModsContainer.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.UI; -using System.Collections.Generic; -using System.Linq; - -namespace osu.Game.Overlays.Profile.Sections.Ranks -{ - public class ScoreModsContainer : FlowContainer - { - protected override IEnumerable ComputeLayoutPositions() - { - int count = FlowingChildren.Count(); - for (int i = 0; i < count; i++) - yield return new Vector2(DrawWidth * i * (count == 1 ? 0 : 1f / (count - 1)), 0); - } - } -} diff --git a/osu.Game/Overlays/Rankings/DismissableFlag.cs b/osu.Game/Overlays/Rankings/DismissableFlag.cs new file mode 100644 index 0000000000..7a55b0bba6 --- /dev/null +++ b/osu.Game/Overlays/Rankings/DismissableFlag.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Users.Drawables; +using osuTK.Graphics; +using osuTK; +using osu.Framework.Input.Events; +using System; + +namespace osu.Game.Overlays.Rankings +{ + public class DismissableFlag : UpdateableFlag + { + private const int duration = 200; + + public Action Action; + + private readonly SpriteIcon hoverIcon; + + public DismissableFlag() + { + AddInternal(hoverIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Depth = -1, + Alpha = 0, + Size = new Vector2(10), + Icon = FontAwesome.Solid.Times, + }); + } + + protected override bool OnHover(HoverEvent e) + { + hoverIcon.FadeIn(duration, Easing.OutQuint); + this.FadeColour(Color4.Gray, duration, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + hoverIcon.FadeOut(duration, Easing.OutQuint); + this.FadeColour(Color4.White, duration, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + Action?.Invoke(); + return true; + } + } +} diff --git a/osu.Game/Overlays/Rankings/HeaderTitle.cs b/osu.Game/Overlays/Rankings/HeaderTitle.cs new file mode 100644 index 0000000000..cba407ecf7 --- /dev/null +++ b/osu.Game/Overlays/Rankings/HeaderTitle.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Users; +using osu.Framework.Graphics; +using osuTK; +using osu.Game.Graphics; +using osu.Framework.Allocation; + +namespace osu.Game.Overlays.Rankings +{ + public class HeaderTitle : CompositeDrawable + { + private const int spacing = 10; + private const int flag_margin = 5; + private const int text_size = 40; + + public readonly Bindable Scope = new Bindable(); + public readonly Bindable Country = new Bindable(); + + private readonly SpriteText scopeText; + private readonly DismissableFlag flag; + + public HeaderTitle() + { + AutoSizeAxes = Axes.Both; + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(spacing, 0), + Children = new Drawable[] + { + flag = new DismissableFlag + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Bottom = flag_margin }, + Size = new Vector2(30, 20), + }, + scopeText = new SpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Light) + }, + new SpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Light), + Text = @"Ranking" + } + } + }; + + flag.Action += () => Country.Value = null; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + scopeText.Colour = colours.Lime; + } + + protected override void LoadComplete() + { + Scope.BindValueChanged(onScopeChanged, true); + Country.BindValueChanged(onCountryChanged, true); + base.LoadComplete(); + } + + private void onScopeChanged(ValueChangedEvent scope) + { + scopeText.Text = scope.NewValue.ToString(); + + if (scope.NewValue != RankingsScope.Performance) + Country.Value = null; + } + + private void onCountryChanged(ValueChangedEvent country) + { + if (country.NewValue == null) + { + flag.Hide(); + return; + } + + Scope.Value = RankingsScope.Performance; + + flag.Country = country.NewValue; + flag.Show(); + } + } +} diff --git a/osu.Game/Overlays/Rankings/RankingsHeader.cs b/osu.Game/Overlays/Rankings/RankingsHeader.cs new file mode 100644 index 0000000000..6aa3e75df9 --- /dev/null +++ b/osu.Game/Overlays/Rankings/RankingsHeader.cs @@ -0,0 +1,129 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Rulesets; +using osu.Game.Users; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osuTK; +using osu.Game.Graphics.UserInterface; +using System.Collections.Generic; + +namespace osu.Game.Overlays.Rankings +{ + public class RankingsHeader : CompositeDrawable + { + private const int content_height = 250; + + public IEnumerable Spotlights + { + get => dropdown.Items; + set => dropdown.Items = value; + } + + public readonly Bindable Scope = new Bindable(); + public readonly Bindable Ruleset = new Bindable(); + public readonly Bindable Country = new Bindable(); + public readonly Bindable Spotlight = new Bindable(); + + private readonly OsuDropdown dropdown; + + public RankingsHeader() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + AddInternal(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new RankingsRulesetSelector + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Current = Ruleset + }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = content_height, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = new HeaderBackground(), + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Children = new Drawable[] + { + new RankingsScopeSelector + { + Margin = new MarginPadding { Top = 10 }, + Current = Scope + }, + new HeaderTitle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = 10 }, + Scope = { BindTarget = Scope }, + Country = { BindTarget = Country }, + }, + dropdown = new OsuDropdown + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Width = 0.8f, + Current = Spotlight, + } + } + }, + } + } + } + }); + } + + protected override void LoadComplete() + { + Scope.BindValueChanged(onScopeChanged, true); + base.LoadComplete(); + } + + private void onScopeChanged(ValueChangedEvent scope) => + dropdown.FadeTo(scope.NewValue == RankingsScope.Spotlights ? 1 : 0, 200, Easing.OutQuint); + + private class HeaderBackground : Sprite + { + public HeaderBackground() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fill; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get(@"Headers/rankings"); + } + } + } +} diff --git a/osu.Game/Overlays/Rankings/RankingsRulesetSelector.cs b/osu.Game/Overlays/Rankings/RankingsRulesetSelector.cs new file mode 100644 index 0000000000..3d25e3995a --- /dev/null +++ b/osu.Game/Overlays/Rankings/RankingsRulesetSelector.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osuTK; +using System.Linq; + +namespace osu.Game.Overlays.Rankings +{ + public class RankingsRulesetSelector : PageTabControl + { + protected override TabItem CreateTabItem(RulesetInfo value) => new RankingsTabItem(value); + + protected override Dropdown CreateDropdown() => null; + + public RankingsRulesetSelector() + { + AutoSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, RulesetStore rulesets) + { + foreach (var r in rulesets.AvailableRulesets) + AddItem(r); + + AccentColour = colours.Lime; + + SelectTab(TabContainer.FirstOrDefault()); + } + + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20, 0), + }; + + private class RankingsTabItem : PageTabItem + { + public RankingsTabItem(RulesetInfo value) + : base(value) + { + } + + protected override string CreateText() => $"{Value.Name}"; + } + } +} diff --git a/osu.Game/Overlays/Rankings/RankingsScopeSelector.cs b/osu.Game/Overlays/Rankings/RankingsScopeSelector.cs new file mode 100644 index 0000000000..2095bcc61c --- /dev/null +++ b/osu.Game/Overlays/Rankings/RankingsScopeSelector.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics.UserInterface; +using osu.Framework.Allocation; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Rankings +{ + public class RankingsScopeSelector : GradientLineTabControl + { + [BackgroundDependencyLoader] + private void load() + { + AccentColour = LineColour = Color4.Black; + } + } + + public enum RankingsScope + { + Performance, + Spotlights, + Score, + Country + } +} diff --git a/osu.Game/Overlays/Rankings/Spotlight.cs b/osu.Game/Overlays/Rankings/Spotlight.cs new file mode 100644 index 0000000000..e956b4f449 --- /dev/null +++ b/osu.Game/Overlays/Rankings/Spotlight.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; + +namespace osu.Game.Overlays.Rankings +{ + public class Spotlight + { + [JsonProperty("id")] + public int Id; + + [JsonProperty("text")] + public string Text; + + public override string ToString() => Text; + } +} diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 66fec1ecf9..b02b1a5489 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.General api?.Register(this); } - public void APIStateChanged(IAPIProvider api, APIState state) + public void APIStateChanged(IAPIProvider api, APIState state) => Schedule(() => { form = null; @@ -184,7 +184,7 @@ namespace osu.Game.Overlays.Settings.Sections.General } if (form != null) GetContainingInputManager()?.ChangeFocus(form); - } + }); public override bool AcceptsFocus => true; diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index 01cdc9aa32..ea2811e5cd 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -23,8 +23,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, new SettingsCheckbox { - LabelText = "Rotate cursor when dragging", - Bindable = config.GetBindable(OsuSetting.CursorRotation) + LabelText = "Video", + Bindable = config.GetBindable(OsuSetting.ShowVideoBackground) + }, + new SettingsCheckbox + { + LabelText = "Hit Lighting", + Bindable = config.GetBindable(OsuSetting.HitLighting) }, new SettingsEnumDropdown { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/MainMenuSettings.cs deleted file mode 100644 index 92f64d0e14..0000000000 --- a/osu.Game/Overlays/Settings/Sections/Graphics/MainMenuSettings.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Game.Configuration; - -namespace osu.Game.Overlays.Settings.Sections.Graphics -{ - public class MainMenuSettings : SettingsSubsection - { - protected override string Header => "User Interface"; - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - Children = new[] - { - new SettingsCheckbox - { - LabelText = "Parallax", - Bindable = config.GetBindable(OsuSetting.MenuParallax) - }, - }; - } - } -} diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs new file mode 100644 index 0000000000..a6956b7d9a --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Settings.Sections.Graphics +{ + public class UserInterfaceSettings : SettingsSubsection + { + protected override string Header => "User Interface"; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = "Rotate cursor when dragging", + Bindable = config.GetBindable(OsuSetting.CursorRotation) + }, + new SettingsCheckbox + { + LabelText = "Parallax", + Bindable = config.GetBindable(OsuSetting.MenuParallax) + }, + new SettingsSlider + { + LabelText = "Hold-to-confirm activation time", + Bindable = config.GetBindable(OsuSetting.UIHoldActivationDelay), + KeyboardStep = 50 + }, + }; + } + + private class TimeSlider : OsuSliderBar + { + public override string TooltipText => Current.Value.ToString("N0") + "ms"; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index 3d6086d3ea..89caa3dc8f 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections new RendererSettings(), new LayoutSettings(), new DetailSettings(), - new MainMenuSettings(), + new UserInterfaceSettings(), }; } } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 35be930a2e..d3029d8ab9 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -35,16 +35,16 @@ namespace osu.Game.Overlays.Settings.Sections Children = new Drawable[] { skinDropdown = new SkinSettingsDropdown(), - new SettingsSlider + new SettingsSlider { LabelText = "Menu cursor size", - Bindable = config.GetBindable(OsuSetting.MenuCursorSize), + Bindable = config.GetBindable(OsuSetting.MenuCursorSize), KeyboardStep = 0.01f }, - new SettingsSlider + new SettingsSlider { LabelText = "Gameplay cursor size", - Bindable = config.GetBindable(OsuSetting.GameplayCursorSize), + Bindable = config.GetBindable(OsuSetting.GameplayCursorSize), KeyboardStep = 0.01f }, new SettingsCheckbox @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Settings.Sections } } - private class SizeSlider : OsuSliderBar + private class SizeSlider : OsuSliderBar { public override string TooltipText => Current.Value.ToString(@"0.##x"); } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 4f8cb7660b..8289ca175d 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Judgements /// public class DrawableJudgement : CompositeDrawable { - private const float judgement_size = 80; + private const float judgement_size = 128; private OsuColour colours; @@ -34,10 +34,14 @@ namespace osu.Game.Rulesets.Judgements /// /// Duration of initial fade in. - /// Default fade out will start immediately after this duration. /// protected virtual double FadeInDuration => 100; + /// + /// Duration to wait until fade out begins. Defaults to . + /// + protected virtual double FadeOutDelay => FadeInDuration; + /// /// Creates a drawable which visualises a . /// @@ -64,10 +68,10 @@ namespace osu.Game.Rulesets.Judgements Child = new SkinnableDrawable(new GameplaySkinComponent(Result.Type), _ => JudgementText = new OsuSpriteText { Text = Result.Type.GetDescription().ToUpperInvariant(), - Font = OsuFont.Numeric.With(size: 12), + Font = OsuFont.Numeric.With(size: 20), Colour = judgementColour(Result.Type), Scale = new Vector2(0.85f, 1), - }) + }, confineMode: ConfineMode.NoScaling) }; } @@ -76,7 +80,7 @@ namespace osu.Game.Rulesets.Judgements JudgementBody.ScaleTo(0.9f); JudgementBody.ScaleTo(1, 500, Easing.OutElastic); - this.Delay(FadeInDuration).FadeOut(400); + this.Delay(FadeOutDelay).FadeOut(400); } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 56dc121b17..59a7917e55 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -51,6 +51,11 @@ namespace osu.Game.Rulesets.Judgements /// public double HealthAtJudgement { get; internal set; } + /// + /// Whether the user was in a failed state prior to this occurring. + /// + public bool FailedAtJudgement { get; internal set; } + /// /// Whether a miss or hit occurred. /// diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs index c0262b783b..120bfc9a23 100644 --- a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs +++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs @@ -12,5 +12,10 @@ namespace osu.Game.Rulesets.Mods /// Whether we should allow failing at the current point in time. /// bool AllowFail { get; } + + /// + /// Whether we want to restart on fail. Only used if is true. + /// + bool RestartOnFail { get; } } } diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index e70d58acea..070a10b1c8 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -26,7 +26,10 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.Automation; public override string Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; + public bool AllowFail => false; + public bool RestartOnFail => false; + public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModBlockFail.cs b/osu.Game/Rulesets/Mods/ModBlockFail.cs index 26efc3932d..7d7ecfa416 100644 --- a/osu.Game/Rulesets/Mods/ModBlockFail.cs +++ b/osu.Game/Rulesets/Mods/ModBlockFail.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Mods /// public bool AllowFail => false; + public bool RestartOnFail => false; + public void ReadFromConfig(OsuConfigManager config) { showHealthBar = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail); diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 56ec0bec06..a55ebc51d6 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -2,13 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModEasy : Mod, IApplicableToDifficulty + public abstract class ModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToScoreProcessor { public override string Name => "Easy"; public override string Acronym => "EZ"; @@ -18,6 +21,10 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; + private int retries = 2; + + private BindableNumber health; + public void ApplyToDifficulty(BeatmapDifficulty difficulty) { const float ratio = 0.5f; @@ -26,5 +33,27 @@ namespace osu.Game.Rulesets.Mods difficulty.DrainRate *= ratio; difficulty.OverallDifficulty *= ratio; } + + public bool AllowFail + { + get + { + if (retries == 0) return true; + + health.Value = health.MaxValue; + retries--; + + return false; + } + } + + public bool RestartOnFail => false; + + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + { + health = scoreProcessor.Health.GetBoundCopy(); + } + + public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; } } diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index e332abd914..c4c4ab1f04 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -10,7 +10,7 @@ using osu.Game.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModSuddenDeath : Mod, IApplicableToScoreProcessor + public abstract class ModSuddenDeath : Mod, IApplicableToScoreProcessor, IApplicableFailOverride { public override string Name => "Sudden Death"; public override string Acronym => "SD"; @@ -21,6 +21,9 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; + public bool AllowFail => true; + public bool RestartOnFail => true; + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { scoreProcessor.FailConditions += FailCondition; diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs new file mode 100644 index 0000000000..4f9395435e --- /dev/null +++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Rulesets.Objects +{ + public class BarLineGenerator + where TBarLine : class, IBarLine, new() + { + /// + /// The generated bar lines. + /// + public readonly List BarLines = new List(); + + /// + /// Constructs and generates bar lines for provided beatmap. + /// + /// The beatmap to generate bar lines for. + public BarLineGenerator(IBeatmap beatmap) + { + if (beatmap.HitObjects.Count == 0) + return; + + HitObject lastObject = beatmap.HitObjects.Last(); + double lastHitTime = 1 + ((lastObject as IHasEndTime)?.EndTime ?? lastObject.StartTime); + + var timingPoints = beatmap.ControlPointInfo.TimingPoints; + + if (timingPoints.Count == 0) + return; + + for (int i = 0; i < timingPoints.Count; i++) + { + TimingControlPoint currentTimingPoint = timingPoints[i]; + int currentBeat = 0; + + // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object + double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature; + + double barLength = currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature; + + for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++) + { + BarLines.Add(new TBarLine + { + StartTime = t, + Major = currentBeat % (int)currentTimingPoint.TimeSignature == 0 + }); + } + } + } + } +} diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1a224b2cea..b94de0df89 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -76,8 +76,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public JudgementResult Result { get; private set; } - private bool judgementOccurred; - public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; protected override bool RequiresChildrenUpdate => true; @@ -155,6 +153,8 @@ namespace osu.Game.Rulesets.Objects.Drawables if (UseTransformStateManagement) { + LifetimeEnd = double.MaxValue; + double transformTime = HitObject.StartTime - InitialLifetimeOffset; base.ApplyTransformsAt(transformTime, true); @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { UpdateInitialTransforms(); - var judgementOffset = Math.Min(HitObject.HitWindows?.HalfWindowFor(HitResult.Miss) ?? double.MaxValue, Result?.TimeOffset ?? 0); + var judgementOffset = Math.Min(HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? double.MaxValue, Result?.TimeOffset ?? 0); using (BeginDelayedSequence(InitialLifetimeOffset + judgementOffset, true)) { @@ -172,6 +172,9 @@ namespace osu.Game.Rulesets.Objects.Drawables state.Value = newState; } } + + if (state.Value != ArmedState.Idle && LifetimeEnd == double.MaxValue) + Expire(); } else state.Value = newState; @@ -202,6 +205,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Apply transforms based on the current . Previous states are automatically cleared. + /// In the case of a non-idle , and if was not set during this call, will be invoked. /// /// The new armed state. protected virtual void UpdateStateTransforms(ArmedState state) @@ -236,12 +240,30 @@ namespace osu.Game.Rulesets.Objects.Drawables #endregion - protected override void SkinChanged(ISkinSource skin, bool allowFallback) + protected sealed override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); if (HitObject is IHasComboInformation combo) - AccentColour.Value = skin.GetValue(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White; + { + var comboColours = skin.GetConfig>(GlobalSkinConfiguration.ComboColours)?.Value; + + AccentColour.Value = comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White; + } + + ApplySkin(skin, allowFallback); + + if (IsLoaded) + updateState(State.Value, true); + } + + /// + /// Called when a change is made to the skin. + /// + /// The new skin. + /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. + protected virtual void ApplySkin(ISkinSource skin, bool allowFallback) + { } /// @@ -306,7 +328,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// This is only used as an optimisation to delay the initial update of this and may be tuned more aggressively if required. /// It is indirectly used to decide the automatic transform offset provided to . - /// A more accurate should be set inside for an state. + /// A more accurate should be set for further optimisation (in , for example). /// protected virtual double InitialLifetimeOffset => 10000; @@ -342,8 +364,6 @@ namespace osu.Game.Rulesets.Objects.Drawables if (!Result.HasResult) throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); - judgementOccurred = true; - // Ensure that the judgement is given a valid time offset, because this may not get set by the caller var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; Result.TimeOffset = Time.Current - endTime; @@ -376,21 +396,13 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Time.Elapsed < 0) return false; - judgementOccurred = false; - - if (AllJudged) + if (Judged) return false; - foreach (var d in NestedHitObjects) - judgementOccurred |= d.UpdateResult(userTriggered); - - if (judgementOccurred || Judged) - return judgementOccurred; - var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; CheckForResult(userTriggered, Time.Current - endTime); - return judgementOccurred; + return Judged; } /// diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 5e029139d9..96297ab44f 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects { diff --git a/osu.Game/Rulesets/Objects/IBarLine.cs b/osu.Game/Rulesets/Objects/IBarLine.cs new file mode 100644 index 0000000000..14df80e3b9 --- /dev/null +++ b/osu.Game/Rulesets/Objects/IBarLine.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Objects +{ + /// + /// Interface for bar line hitobjects. + /// Used to decouple bar line generation from ruleset-specific rendering/drawing hierarchies. + /// + public interface IBarLine + { + /// + /// The time position of the bar. + /// + double StartTime { set; } + + /// + /// Whether this bar line is a prominent beat (based on time signature of beatmap). + /// + bool Major { set; } + } +} diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs index 06fde576d2..609bdd571a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects.Legacy.Mania { diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs index 096c07f7d2..350ee3185d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects.Legacy.Mania { diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs index 226d91bb86..e372fbd273 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects.Legacy.Mania { diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs index eb20fa67f1..067377d300 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects.Legacy.Mania { diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs index 84b66a4c26..c9851a0074 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Objects.Legacy.Osu diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index c850feb189..1c1180702b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Objects.Legacy.Osu diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index e5a8884aa2..bc94ea1803 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Objects.Legacy.Osu diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs index 5cecc2a59f..709345170f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Scoring; + namespace osu.Game.Rulesets.Objects.Legacy.Taiko { /// diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs index 5cedc6e2e5..c173b3e11a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Scoring; + namespace osu.Game.Rulesets.Objects.Legacy.Taiko { /// diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs index ca9fdd53ed..9a35ad2776 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects.Legacy.Taiko { diff --git a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs index 7ecdc0715b..c2947c0aca 100644 --- a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs @@ -14,8 +14,9 @@ namespace osu.Game.Rulesets.Replays.Types /// /// Populates this using values from a . /// - /// The to extract values from. + /// The to extract values from. /// The beatmap. - void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap); + /// The last post-conversion , used to fill in missing delta information. May be null. + void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null); } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index b63292757d..dd1b3615c7 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; +using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Edit; @@ -83,6 +84,8 @@ namespace osu.Game.Rulesets public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle }; + public virtual IResourceStore CreateResourceStore() => new NamespacedResourceStore(new DllResourceStore(GetType().Assembly.Location), @"Resources"); + public abstract string Description { get; } public virtual RulesetSettingsSubsection CreateSettings() => null; diff --git a/osu.Game/Rulesets/RulesetConfigCache.cs b/osu.Game/Rulesets/RulesetConfigCache.cs index 8c9e3c94e2..cdcd2666cf 100644 --- a/osu.Game/Rulesets/RulesetConfigCache.cs +++ b/osu.Game/Rulesets/RulesetConfigCache.cs @@ -36,5 +36,14 @@ namespace osu.Game.Rulesets return configCache.GetOrAdd(ruleset.RulesetInfo.ID.Value, _ => ruleset.CreateConfig(settingsStore)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + // ensures any potential database operations are finalised before game destruction. + foreach (var c in configCache.Values) + (c as IDisposable)?.Dispose(); + } } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index d9cff86265..6a69fd8dd0 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -20,7 +20,12 @@ namespace osu.Game.Rulesets [JsonIgnore] public bool Available { get; set; } - public virtual Ruleset CreateInstance() => (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo), this); + public virtual Ruleset CreateInstance() + { + if (!Available) return null; + + return (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo), this); + } public bool Equals(RulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 2376f12e9e..7ba88d3df8 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -16,6 +16,10 @@ namespace osu.Game.Rulesets.Scoring /// /// Indicates that the object has been judged as a miss. /// + /// + /// This miss window should determine how early a hit can be before it is considered for judgement (as opposed to being ignored as + /// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time). + /// [Description(@"Miss")] Miss, diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs similarity index 55% rename from osu.Game/Rulesets/Objects/HitWindows.cs rename to osu.Game/Rulesets/Scoring/HitWindows.cs index e88af67c7c..efc4cd9f5c 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -4,51 +4,31 @@ using System; using System.Collections.Generic; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Objects; -namespace osu.Game.Rulesets.Objects +namespace osu.Game.Rulesets.Scoring { + /// + /// A structure containing timing data for hit window based gameplay. + /// public class HitWindows { - private static readonly IReadOnlyDictionary base_ranges = new Dictionary + private static readonly DifficultyRange[] base_ranges = { - { HitResult.Perfect, (44.8, 38.8, 27.8) }, - { HitResult.Great, (128, 98, 68) }, - { HitResult.Good, (194, 164, 134) }, - { HitResult.Ok, (254, 224, 194) }, - { HitResult.Meh, (302, 272, 242) }, - { HitResult.Miss, (376, 346, 316) }, + new DifficultyRange(HitResult.Perfect, 22.4D, 19.4D, 13.9D), + new DifficultyRange(HitResult.Great, 64, 49, 34), + new DifficultyRange(HitResult.Good, 97, 82, 67), + new DifficultyRange(HitResult.Ok, 127, 112, 97), + new DifficultyRange(HitResult.Meh, 151, 136, 121), + new DifficultyRange(HitResult.Miss, 188, 173, 158), }; - /// - /// Hit window for a result. - /// - public double Perfect { get; protected set; } - - /// - /// Hit window for a result. - /// - public double Great { get; protected set; } - - /// - /// Hit window for a result. - /// - public double Good { get; protected set; } - - /// - /// Hit window for an result. - /// - public double Ok { get; protected set; } - - /// - /// Hit window for a result. - /// - public double Meh { get; protected set; } - - /// - /// Hit window for a result. - /// - public double Miss { get; protected set; } + private double perfect; + private double great; + private double good; + private double ok; + private double meh; + private double miss; /// /// Retrieves the with the largest hit window that produces a successful hit. @@ -66,15 +46,15 @@ namespace osu.Game.Rulesets.Objects } /// - /// Retrieves a mapping of s to their half window timing for all allowed s. + /// Retrieves a mapping of s to their timing windows for all allowed s. /// /// - public IEnumerable<(HitResult result, double length)> GetAllAvailableHalfWindows() + public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) { if (IsHitResultAllowed(result)) - yield return (result, HalfWindowFor(result)); + yield return (result, WindowFor(result)); } } @@ -83,31 +63,45 @@ namespace osu.Game.Rulesets.Objects /// /// The result type to check. /// Whether the can be achieved. - public virtual bool IsHitResultAllowed(HitResult result) - { - switch (result) - { - case HitResult.Perfect: - case HitResult.Ok: - return false; - - default: - return true; - } - } + public virtual bool IsHitResultAllowed(HitResult result) => true; /// /// Sets hit windows with values that correspond to a difficulty parameter. /// /// The parameter. - public virtual void SetDifficulty(double difficulty) + public void SetDifficulty(double difficulty) { - Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]); - Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); - Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); - Ok = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Ok]); - Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]); - Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]); + foreach (var range in GetRanges()) + { + var value = BeatmapDifficulty.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); + + switch (range.Result) + { + case HitResult.Miss: + miss = value; + break; + + case HitResult.Meh: + meh = value; + break; + + case HitResult.Ok: + ok = value; + break; + + case HitResult.Good: + good = value; + break; + + case HitResult.Great: + great = value; + break; + + case HitResult.Perfect: + perfect = value; + break; + } + } } /// @@ -121,7 +115,7 @@ namespace osu.Game.Rulesets.Objects for (var result = HitResult.Perfect; result >= HitResult.Miss; --result) { - if (IsHitResultAllowed(result) && timeOffset <= HalfWindowFor(result)) + if (IsHitResultAllowed(result) && timeOffset <= WindowFor(result)) return result; } @@ -129,32 +123,32 @@ namespace osu.Game.Rulesets.Objects } /// - /// Retrieves half the hit window for a . - /// This is useful if the hit window for one half of the hittable range of a is required. + /// Retrieves the hit window for a . + /// This is the number of +/- milliseconds allowed for the requested result (so the actual hittable range is double this). /// /// The expected . /// One half of the hit window for . - public double HalfWindowFor(HitResult result) + public double WindowFor(HitResult result) { switch (result) { case HitResult.Perfect: - return Perfect / 2; + return perfect; case HitResult.Great: - return Great / 2; + return great; case HitResult.Good: - return Good / 2; + return good; case HitResult.Ok: - return Ok / 2; + return ok; case HitResult.Meh: - return Meh / 2; + return meh; case HitResult.Miss: - return Miss / 2; + return miss; default: throw new ArgumentException(nameof(result)); @@ -167,6 +161,30 @@ namespace osu.Game.Rulesets.Objects /// /// The time offset. /// Whether the can be hit at any point in the future from this time offset. - public bool CanBeHit(double timeOffset) => timeOffset <= HalfWindowFor(LowestSuccessfulHitResult()); + public bool CanBeHit(double timeOffset) => timeOffset <= WindowFor(LowestSuccessfulHitResult()); + + /// + /// Retrieve a valid list of s representing hit windows. + /// Defaults are provided but can be overridden to customise for a ruleset. + /// + protected virtual DifficultyRange[] GetRanges() => base_ranges; + } + + public struct DifficultyRange + { + public readonly HitResult Result; + + public double Min; + public double Average; + public double Max; + + public DifficultyRange(HitResult result, double min, double average, double max) + { + Result = result; + + Min = min; + Average = average; + Max = max; + } } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 86c2c07f2a..18c2a2ca01 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The current health. /// - public readonly BindableDouble Health = new BindableDouble { MinValue = 0, MaxValue = 1 }; + public readonly BindableDouble Health = new BindableDouble(1) { MinValue = 0, MaxValue = 1 }; /// /// The current combo. @@ -92,6 +92,11 @@ namespace osu.Game.Rulesets.Scoring /// public virtual bool HasCompleted => false; + /// + /// The total number of judged s at the current point in time. + /// + public int JudgedHits { get; protected set; } + /// /// Whether this ScoreProcessor has already triggered the failed state. /// @@ -142,6 +147,8 @@ namespace osu.Game.Rulesets.Scoring Rank.Value = ScoreRank.X; HighestCombo.Value = 0; + JudgedHits = 0; + HasFailed = false; } @@ -193,7 +200,7 @@ namespace osu.Game.Rulesets.Scoring score.Statistics[result] = GetStatistic(result); } - protected abstract int GetStatistic(HitResult result); + public abstract int GetStatistic(HitResult result); public abstract double GetStandardisedScore(); } @@ -208,7 +215,6 @@ namespace osu.Game.Rulesets.Scoring public sealed override bool HasCompleted => JudgedHits == MaxHits; protected int MaxHits { get; private set; } - protected int JudgedHits { get; private set; } private double maxHighestCombo; @@ -227,6 +233,8 @@ namespace osu.Game.Rulesets.Scoring drawableRuleset.OnRevertResult += revertResult; ApplyBeatmap(drawableRuleset.Beatmap); + + Reset(false); SimulateAutoplay(drawableRuleset.Beatmap); Reset(true); @@ -322,6 +330,10 @@ namespace osu.Game.Rulesets.Scoring result.ComboAtJudgement = Combo.Value; result.HighestComboAtJudgement = HighestCombo.Value; result.HealthAtJudgement = Health.Value; + result.FailedAtJudgement = HasFailed; + + if (HasFailed) + return; JudgedHits++; @@ -369,6 +381,11 @@ namespace osu.Game.Rulesets.Scoring HighestCombo.Value = result.HighestComboAtJudgement; Health.Value = result.HealthAtJudgement; + // Todo: Revert HasFailed state with proper player support + + if (result.FailedAtJudgement) + return; + JudgedHits--; if (result.Judgement.IsBonus) @@ -415,7 +432,7 @@ namespace osu.Game.Rulesets.Scoring } } - protected override int GetStatistic(HitResult result) => scoreResultCounts.GetOrDefault(result); + public override int GetStatistic(HitResult result) => scoreResultCounts.GetOrDefault(result); public override double GetStandardisedScore() => getScore(ScoringMode.Standardised); @@ -432,7 +449,6 @@ namespace osu.Game.Rulesets.Scoring base.Reset(storeResults); - JudgedHits = 0; baseScore = 0; rollingMaxBaseScore = 0; bonusScore = 0; diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index a32407d180..d68b0e94c5 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -11,13 +11,20 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.IO.Stores; using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Input.Handlers; @@ -51,6 +58,10 @@ namespace osu.Game.Rulesets.UI private readonly Lazy playfield; + private TextureStore textureStore; + + private ISampleStore localSampleStore; + /// /// The playfield. /// @@ -142,6 +153,18 @@ namespace osu.Game.Rulesets.UI { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + var resources = Ruleset.CreateResourceStore(); + + if (resources != null) + { + textureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore(resources, "Textures"))); + textureStore.AddStore(dependencies.Get()); + dependencies.Cache(textureStore); + + localSampleStore = dependencies.Get().GetSampleStore(new NamespacedResourceStore(resources, "Samples")); + dependencies.CacheAs(new FallbackSampleStore(localSampleStore, dependencies.Get())); + } + onScreenDisplay = dependencies.Get(); Config = dependencies.Get().GetConfigFor(Ruleset); @@ -314,6 +337,8 @@ namespace osu.Game.Rulesets.UI { base.Dispose(isDisposing); + localSampleStore?.Dispose(); + if (Config != null) { onScreenDisplay?.StopTracking(this, Config); @@ -443,4 +468,50 @@ namespace osu.Game.Rulesets.UI { } } + + /// + /// A sample store which adds a fallback source. + /// + /// + /// This is a temporary implementation to workaround ISampleStore limitations. + /// + public class FallbackSampleStore : ISampleStore + { + private readonly ISampleStore primary; + private readonly ISampleStore secondary; + + public FallbackSampleStore(ISampleStore primary, ISampleStore secondary) + { + this.primary = primary; + this.secondary = secondary; + } + + public SampleChannel Get(string name) => primary.Get(name) ?? secondary.Get(name); + + public Task GetAsync(string name) => primary.GetAsync(name) ?? secondary.GetAsync(name); + + public Stream GetStream(string name) => primary.GetStream(name) ?? secondary.GetStream(name); + + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + + public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => throw new NotImplementedException(); + + public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => throw new NotImplementedException(); + + public BindableDouble Volume => throw new NotImplementedException(); + + public BindableDouble Balance => throw new NotImplementedException(); + + public BindableDouble Frequency => throw new NotImplementedException(); + + public int PlaybackConcurrency + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + public void Dispose() + { + } + } } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index e25c3bd0e7..98e27240d3 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -137,9 +137,9 @@ namespace osu.Game.Rulesets.UI { } - public bool OnPressed(T action) => Target.Children.OfType>().Any(c => c.OnPressed(action)); + public bool OnPressed(T action) => Target.Children.OfType>().Any(c => c.OnPressed(action, Clock.ElapsedFrameTime > 0)); - public bool OnReleased(T action) => Target.Children.OfType>().Any(c => c.OnReleased(action)); + public bool OnReleased(T action) => Target.Children.OfType>().Any(c => c.OnReleased(action, Clock.ElapsedFrameTime > 0)); } #endregion diff --git a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreParser.cs b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreParser.cs index 77edd24612..2115d784a0 100644 --- a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreParser.cs +++ b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreParser.cs @@ -22,6 +22,6 @@ namespace osu.Game.Scoring.Legacy } protected override Ruleset GetRuleset(int rulesetId) => rulesets.GetRuleset(rulesetId).CreateInstance(); - protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmaps.GetWorkingBeatmap(beatmaps.QueryBeatmap(b => b.MD5Hash == md5Hash)); + protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmaps.GetWorkingBeatmap(beatmaps.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.MD5Hash == md5Hash)); } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs index 2e4b4b3a9a..2cdd0c4b5e 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreParser.cs @@ -218,6 +218,7 @@ namespace osu.Game.Scoring.Legacy private void readLegacyReplay(Replay replay, StreamReader reader) { float lastTime = 0; + ReplayFrame currentFrame = null; foreach (var l in reader.ReadToEnd().Split(',')) { @@ -240,23 +241,25 @@ namespace osu.Game.Scoring.Legacy if (diff < 0) continue; - replay.Frames.Add(convertFrame(new LegacyReplayFrame(lastTime, + currentFrame = convertFrame(new LegacyReplayFrame(lastTime, Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE), Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE), - (ReplayButtonState)Parsing.ParseInt(split[3])))); + (ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame); + + replay.Frames.Add(currentFrame); } } - private ReplayFrame convertFrame(LegacyReplayFrame legacyFrame) + private ReplayFrame convertFrame(LegacyReplayFrame currentFrame, ReplayFrame lastFrame) { var convertible = currentRuleset.CreateConvertibleReplayFrame(); if (convertible == null) throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}"); - convertible.ConvertFrom(legacyFrame, currentBeatmap); + convertible.ConvertFrom(currentFrame, currentBeatmap, lastFrame); var frame = (ReplayFrame)convertible; - frame.Time = legacyFrame.Time; + frame.Time = currentFrame.Time; return frame; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 266725a739..d3c37bd4f4 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -11,8 +11,8 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Users; using osu.Game.Rulesets.Scoring; +using osu.Game.Users; namespace osu.Game.Scoring { diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 5225740d0b..2730b0b90d 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -178,7 +178,7 @@ namespace osu.Game.Screens.Backgrounds BlurAmount.ValueChanged += _ => UpdateVisuals(); } - protected override bool ShowDimContent => !ShowStoryboard.Value || !StoryboardReplacesBackground.Value; // The background needs to be hidden in the case of it being replaced by the storyboard + protected override bool ShowDimContent => !ShowStoryboard.Value || !StoryboardReplacesBackground.Value || !ShowVideo.Value; // The background needs to be hidden in the case of it being replaced by the storyboard protected override void UpdateVisuals() { diff --git a/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs b/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs index 299059407c..4b8720fe1c 100644 --- a/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -33,6 +34,8 @@ namespace osu.Game.Screens.Edit public Texture Background => workingBeatmap.Background; + public VideoSprite Video => workingBeatmap.Video; + public Track Track => workingBeatmap.Track; public Waveform Waveform => workingBeatmap.Waveform; diff --git a/osu.Game/Screens/Edit/Setup/Components/LabelledComponents/LabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/Components/LabelledComponents/LabelledTextBox.cs deleted file mode 100644 index 1c53fc7088..0000000000 --- a/osu.Game/Screens/Edit/Setup/Components/LabelledComponents/LabelledTextBox.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osuTK.Graphics; - -namespace osu.Game.Screens.Edit.Setup.Components.LabelledComponents -{ - public class LabelledTextBox : CompositeDrawable - { - private const float label_container_width = 150; - private const float corner_radius = 15; - private const float default_height = 40; - private const float default_label_left_padding = 15; - private const float default_label_top_padding = 12; - private const float default_label_text_size = 16; - - public event TextBox.OnCommitHandler OnCommit; - - public bool ReadOnly - { - get => textBox.ReadOnly; - set => textBox.ReadOnly = value; - } - - public string LabelText - { - get => label.Text; - set => label.Text = value; - } - - public float LabelTextSize - { - get => label.Font.Size; - set => label.Font = label.Font.With(size: value); - } - - public string PlaceholderText - { - get => textBox.PlaceholderText; - set => textBox.PlaceholderText = value; - } - - public string Text - { - get => textBox.Text; - set => textBox.Text = value; - } - - public Color4 LabelTextColour - { - get => label.Colour; - set => label.Colour = value; - } - - private readonly OsuTextBox textBox; - private readonly OsuSpriteText label; - - public LabelledTextBox() - { - RelativeSizeAxes = Axes.X; - Height = default_height; - CornerRadius = corner_radius; - Masking = true; - - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("1c2125"), - }, - new GridContainer - { - RelativeSizeAxes = Axes.X, - Height = default_height, - Content = new[] - { - new Drawable[] - { - label = new OsuSpriteText - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Padding = new MarginPadding { Left = default_label_left_padding, Top = default_label_top_padding }, - Colour = Color4.White, - Font = OsuFont.GetFont(size: default_label_text_size, weight: FontWeight.Bold), - }, - textBox = new OsuTextBox - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - RelativeSizeAxes = Axes.Both, - Height = 1, - CornerRadius = corner_radius, - }, - }, - }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, label_container_width), - new Dimension() - } - } - } - }; - - textBox.OnCommit += OnCommit; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - textBox.BorderColour = colours.Blue; - } - } -} diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs index 1bf25a2504..ffeadb96c7 100644 --- a/osu.Game/Screens/Menu/Button.cs +++ b/osu.Game/Screens/Menu/Button.cs @@ -31,6 +31,8 @@ namespace osu.Game.Screens.Menu { public event Action StateChanged; + public readonly Key TriggerKey; + private readonly Container iconText; private readonly Container box; private readonly Box boxHoverLayer; @@ -43,7 +45,6 @@ namespace osu.Game.Screens.Menu public ButtonSystemState VisibleState = ButtonSystemState.TopLevel; private readonly Action clickAction; - private readonly Key triggerKey; private SampleChannel sampleClick; private SampleChannel sampleHover; @@ -53,7 +54,7 @@ namespace osu.Game.Screens.Menu { this.sampleName = sampleName; this.clickAction = clickAction; - this.triggerKey = triggerKey; + TriggerKey = triggerKey; AutoSizeAxes = Axes.Both; Alpha = 0; @@ -210,7 +211,7 @@ namespace osu.Game.Screens.Menu if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed) return false; - if (triggerKey == e.Key && triggerKey != Key.Unknown) + if (TriggerKey == e.Key && TriggerKey != Key.Unknown) { trigger(); return true; diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 1a3e1213b4..ed8e4c70f9 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Threading; @@ -180,6 +181,20 @@ namespace osu.Game.Screens.Menu State = ButtonSystemState.Initial; } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (State == ButtonSystemState.Initial) + { + if (buttonsTopLevel.Any(b => e.Key == b.TriggerKey)) + { + logo?.Click(); + return true; + } + } + + return base.OnKeyDown(e); + } + public bool OnPressed(GlobalAction action) { switch (action) diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 073ab639e3..17f999d519 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -173,7 +173,11 @@ namespace osu.Game.Screens.Menu .Then(5500) .FadeOut(250) .ScaleTo(0.9f, 250, Easing.InQuint) - .Finally(d => this.Push(nextScreen)); + .Finally(d => + { + if (nextScreen != null) + this.Push(nextScreen); + }); } } } diff --git a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs index 519834859d..aaa3a77e74 100644 --- a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs +++ b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs @@ -9,6 +9,10 @@ namespace osu.Game.Screens.Menu { public class ExitConfirmOverlay : HoldToConfirmOverlay, IKeyBindingHandler { + protected override bool AllowMultipleFires => true; + + public void Abort() => AbortConfirm(); + public bool OnPressed(GlobalAction action) { if (action == GlobalAction.Back) @@ -24,7 +28,8 @@ namespace osu.Game.Screens.Menu { if (action == GlobalAction.Back) { - AbortConfirm(); + if (!Fired) + AbortConfirm(); return true; } diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 6984959e9c..59ab6ad265 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.Menu Color4 defaultColour = Color4.White.Opacity(0.2f); if (user.Value?.IsSupporter ?? false) - AccentColour = skin.Value.GetValue(s => s.CustomColours.ContainsKey("MenuGlow") ? s.CustomColours["MenuGlow"] : (Color4?)null) ?? defaultColour; + AccentColour = skin.Value.GetConfig(GlobalSkinColour.MenuGlow)?.Value ?? defaultColour; else AccentColour = defaultColour; } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 499b5089f6..dd81569e26 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -1,17 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Charts; using osu.Game.Screens.Edit; @@ -44,15 +49,41 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private MusicController music { get; set; } + [Resolved(canBeNull: true)] + private LoginOverlay login { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved(canBeNull: true)] + private DialogOverlay dialogOverlay { get; set; } + private BackgroundScreenDefault background; protected override BackgroundScreen CreateBackground() => background; + private Bindable holdDelay; + + private ExitConfirmOverlay exitConfirmOverlay; + [BackgroundDependencyLoader(true)] - private void load(DirectOverlay direct, SettingsOverlay settings) + private void load(DirectOverlay direct, SettingsOverlay settings, OsuConfigManager config) { + holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); + if (host.CanExit) - AddInternal(new ExitConfirmOverlay { Action = this.Exit }); + { + AddInternal(exitConfirmOverlay = new ExitConfirmOverlay + { + Action = () => + { + if (holdDelay.Value > 0) + confirmAndExit(); + else + this.Exit(); + } + }); + } AddRangeInternal(new Drawable[] { @@ -67,7 +98,7 @@ namespace osu.Game.Screens.Menu OnEdit = delegate { this.Push(new Editor()); }, OnSolo = onSolo, OnMulti = delegate { this.Push(new Multiplayer()); }, - OnExit = this.Exit, + OnExit = confirmAndExit, } } }, @@ -96,6 +127,12 @@ namespace osu.Game.Screens.Menu preloadSongSelect(); } + private void confirmAndExit() + { + exitConfirmed = true; + this.Exit(); + } + private void preloadSongSelect() { if (songSelect == null) @@ -133,6 +170,9 @@ namespace osu.Game.Screens.Menu Beatmap.ValueChanged += beatmap_ValueChanged; } + private bool loginDisplayed; + private bool exitConfirmed; + protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); @@ -151,6 +191,21 @@ namespace osu.Game.Screens.Menu sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint); } + else if (!api.IsLoggedIn) + { + logo.Action += displayLogin; + } + + bool displayLogin() + { + if (!loginDisplayed) + { + Scheduler.AddDelayed(() => login?.Show(), 500); + loginDisplayed = true; + } + + return true; + } } protected override void LogoSuspending(OsuLogo logo) @@ -197,9 +252,40 @@ namespace osu.Game.Screens.Menu public override bool OnExiting(IScreen next) { + if (!exitConfirmed && dialogOverlay != null && !(dialogOverlay.CurrentDialog is ConfirmExitDialog)) + { + dialogOverlay.Push(new ConfirmExitDialog(confirmAndExit, () => exitConfirmOverlay.Abort())); + return true; + } + buttons.State = ButtonSystemState.Exit; this.FadeOut(3000); return base.OnExiting(next); } + + private class ConfirmExitDialog : PopupDialog + { + public ConfirmExitDialog(Action confirm, Action cancel) + { + HeaderText = "Are you sure you want to exit?"; + BodyText = "Last chance to back out."; + + Icon = FontAwesome.Solid.ExclamationTriangle; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Good bye", + Action = confirm + }, + new PopupDialogCancelButton + { + Text = @"Just a little more", + Action = cancel + }, + }; + } + } } } diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 393964561c..55a6a33e89 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Menu Color4 baseColour = colours.Blue; if (user.Value?.IsSupporter ?? false) - baseColour = skin.Value.GetValue(s => s.CustomColours.ContainsKey("MenuGlow") ? s.CustomColours["MenuGlow"] : (Color4?)null) ?? baseColour; + baseColour = skin.Value.GetConfig(GlobalSkinColour.MenuGlow)?.Value ?? baseColour; // linear colour looks better in this case, so let's use it for now. Color4 gradientDark = baseColour.Opacity(0).ToLinear(); diff --git a/osu.Game/Screens/Play/DimmableVideo.cs b/osu.Game/Screens/Play/DimmableVideo.cs new file mode 100644 index 0000000000..4d6c10d69d --- /dev/null +++ b/osu.Game/Screens/Play/DimmableVideo.cs @@ -0,0 +1,88 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Video; +using osu.Game.Graphics.Containers; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play +{ + public class DimmableVideo : UserDimContainer + { + private readonly VideoSprite video; + private DrawableVideo drawableVideo; + + public DimmableVideo(VideoSprite video) + { + this.video = video; + } + + [BackgroundDependencyLoader] + private void load() + { + initializeVideo(false); + } + + protected override void LoadComplete() + { + ShowVideo.BindValueChanged(_ => initializeVideo(true), true); + base.LoadComplete(); + } + + protected override bool ShowDimContent => ShowVideo.Value && DimLevel < 1; + + private void initializeVideo(bool async) + { + if (video == null) + return; + + if (drawableVideo != null) + return; + + if (!ShowVideo.Value) + return; + + drawableVideo = new DrawableVideo(video); + + if (async) + LoadComponentAsync(drawableVideo, Add); + else + Add(drawableVideo); + } + + private class DrawableVideo : Container + { + public DrawableVideo(VideoSprite video) + { + RelativeSizeAxes = Axes.Both; + Masking = true; + + video.RelativeSizeAxes = Axes.Both; + video.FillMode = FillMode.Fit; + video.Anchor = Anchor.Centre; + video.Origin = Anchor.Centre; + + AddRangeInternal(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + video, + }); + } + + [BackgroundDependencyLoader] + private void load(GameplayClock clock) + { + if (clock != null) + Clock = clock; + } + } + } +} diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index b1948d02d5..379c4c89ed 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -42,5 +42,7 @@ namespace osu.Game.Screens.Play public double FramesPerSecond => underlyingClock.FramesPerSecond; public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; + + public IClock Source => underlyingClock; } } diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index c7e762714c..f54d638584 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -304,6 +304,7 @@ namespace osu.Game.Screens.Play private class Button : DialogButton { + // required to ensure keyboard navigation always starts from an extremity (unless the cursor is moved) protected override bool OnHover(HoverEvent e) => true; protected override bool OnMouseMove(MouseMoveEvent e) @@ -312,5 +313,22 @@ namespace osu.Game.Screens.Play return base.OnMouseMove(e); } } + + [Resolved] + private GlobalActionContainer globalAction { get; set; } + + protected override bool Handle(UIEvent e) + { + switch (e) + { + case ScrollEvent _: + if (ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) + return globalAction.TriggerEvent(e); + + break; + } + + return base.Handle(e); + } } } diff --git a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs index adda94d629..920d11c910 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD.HitErrorMeters; diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 594dd64e52..03a0f23fb6 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osuTK; using osuTK.Graphics; @@ -146,7 +145,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters private void createColourBars(OsuColour colours) { - var windows = HitWindows.GetAllAvailableHalfWindows().ToArray(); + var windows = HitWindows.GetAllAvailableWindows().ToArray(); maxHitWindow = windows.First().length; diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs index da1d9fff0d..dee25048ed 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs @@ -3,7 +3,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD.HitErrorMeters { diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index eee7235a6e..0f9edf5606 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -231,7 +231,6 @@ namespace osu.Game.Screens.Play protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay { - FadeTime = 50, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Margin = new MarginPadding(10), diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/KeyCounter.cs index 88a62ac8d4..f4109a63d0 100644 --- a/osu.Game/Screens/Play/KeyCounter.cs +++ b/osu.Game/Screens/Play/KeyCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -22,9 +20,6 @@ namespace osu.Game.Screens.Play private Container textLayer; private SpriteText countSpriteText; - private readonly List states = new List(); - private KeyCounterState currentState; - public bool IsCounting { get; set; } = true; private int countPresses; @@ -52,20 +47,30 @@ namespace osu.Game.Screens.Play { isLit = value; updateGlowSprite(value); - - if (value && IsCounting) - { - CountPresses++; - saveState(); - } } } } + public void Increment() + { + if (!IsCounting) + return; + + CountPresses++; + } + + public void Decrement() + { + if (!IsCounting) + return; + + CountPresses--; + } + //further: change default values here and in KeyCounterCollection if needed, instead of passing them in every constructor public Color4 KeyDownTextColor { get; set; } = Color4.DarkGray; public Color4 KeyUpTextColor { get; set; } = Color4.White; - public int FadeTime { get; set; } + public double FadeTime { get; set; } protected KeyCounter(string name) { @@ -73,11 +78,8 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(TextureStore textures, GameplayClock clock) + private void load(TextureStore textures) { - if (clock != null) - Clock = clock; - Children = new Drawable[] { buttonSprite = new Sprite @@ -132,42 +134,16 @@ namespace osu.Game.Screens.Play { if (show) { - glowSprite.FadeIn(FadeTime); - textLayer.FadeColour(KeyDownTextColor, FadeTime); + double remainingFadeTime = FadeTime * (1 - glowSprite.Alpha); + glowSprite.FadeIn(remainingFadeTime, Easing.OutQuint); + textLayer.FadeColour(KeyDownTextColor, remainingFadeTime, Easing.OutQuint); } else { - glowSprite.FadeOut(FadeTime); - textLayer.FadeColour(KeyUpTextColor, FadeTime); + double remainingFadeTime = 8 * FadeTime * glowSprite.Alpha; + glowSprite.FadeOut(remainingFadeTime, Easing.OutQuint); + textLayer.FadeColour(KeyUpTextColor, remainingFadeTime, Easing.OutQuint); } } - - public void ResetCount() - { - CountPresses = 0; - states.Clear(); - } - - protected override void Update() - { - base.Update(); - - if (currentState?.Time > Clock.CurrentTime) - restoreStateTo(Clock.CurrentTime); - } - - private void saveState() - { - if (currentState == null || currentState.Time < Clock.CurrentTime) - states.Add(currentState = new KeyCounterState(Clock.CurrentTime, CountPresses)); - } - - private void restoreStateTo(double time) - { - states.RemoveAll(state => state.Time > time); - - currentState = states.LastOrDefault(); - CountPresses = currentState?.Count ?? 0; - } } } diff --git a/osu.Game/Screens/Play/KeyCounterAction.cs b/osu.Game/Screens/Play/KeyCounterAction.cs index 8deac653ad..f60ad7aa5a 100644 --- a/osu.Game/Screens/Play/KeyCounterAction.cs +++ b/osu.Game/Screens/Play/KeyCounterAction.cs @@ -1,11 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Input.Bindings; - namespace osu.Game.Screens.Play { - public class KeyCounterAction : KeyCounter, IKeyBindingHandler + public class KeyCounterAction : KeyCounter where T : struct { public T Action { get; } @@ -16,15 +14,25 @@ namespace osu.Game.Screens.Play Action = action; } - public bool OnPressed(T action) + public bool OnPressed(T action, bool forwards) { - if (action.Equals(Action)) IsLit = true; + if (!action.Equals(Action)) + return false; + + IsLit = true; + if (forwards) + Increment(); return false; } - public bool OnReleased(T action) + public bool OnReleased(T action, bool forwards) { - if (action.Equals(Action)) IsLit = false; + if (!action.Equals(Action)) + return false; + + IsLit = false; + if (!forwards) + Decrement(); return false; } } diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs index d5967f5899..1edb95ca46 100644 --- a/osu.Game/Screens/Play/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/KeyCounterDisplay.cs @@ -17,6 +17,7 @@ namespace osu.Game.Screens.Play public class KeyCounterDisplay : FillFlowContainer { private const int duration = 100; + private const double key_fade_time = 80; public readonly Bindable Visible = new Bindable(true); private readonly Bindable configVisibility = new Bindable(); @@ -33,17 +34,11 @@ namespace osu.Game.Screens.Play base.Add(key); key.IsCounting = IsCounting; - key.FadeTime = FadeTime; + key.FadeTime = key_fade_time; key.KeyDownTextColor = KeyDownTextColor; key.KeyUpTextColor = KeyUpTextColor; } - public void ResetCount() - { - foreach (var counter in Children) - counter.ResetCount(); - } - [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -68,22 +63,6 @@ namespace osu.Game.Screens.Play } } - private int fadeTime; - - public int FadeTime - { - get => fadeTime; - set - { - if (value != fadeTime) - { - fadeTime = value; - foreach (var child in Children) - child.FadeTime = value; - } - } - } - private Color4 keyDownTextColor = Color4.DarkGray; public Color4 KeyDownTextColor @@ -123,11 +102,6 @@ namespace osu.Game.Screens.Play private Receptor receptor; - public Receptor GetReceptor() - { - return receptor ?? (receptor = new Receptor(this)); - } - public void SetReceptor(Receptor receptor) { if (this.receptor != null) diff --git a/osu.Game/Screens/Play/KeyCounterKeyboard.cs b/osu.Game/Screens/Play/KeyCounterKeyboard.cs index d9b6dca79d..29188b6b59 100644 --- a/osu.Game/Screens/Play/KeyCounterKeyboard.cs +++ b/osu.Game/Screens/Play/KeyCounterKeyboard.cs @@ -18,7 +18,12 @@ namespace osu.Game.Screens.Play protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Key == Key) IsLit = true; + if (e.Key == Key) + { + IsLit = true; + Increment(); + } + return base.OnKeyDown(e); } diff --git a/osu.Game/Screens/Play/KeyCounterMouse.cs b/osu.Game/Screens/Play/KeyCounterMouse.cs index 95fa58e5c0..828441de6e 100644 --- a/osu.Game/Screens/Play/KeyCounterMouse.cs +++ b/osu.Game/Screens/Play/KeyCounterMouse.cs @@ -36,7 +36,12 @@ namespace osu.Game.Screens.Play protected override bool OnMouseDown(MouseDownEvent e) { - if (e.Button == Button) IsLit = true; + if (e.Button == Button) + { + IsLit = true; + Increment(); + } + return base.OnMouseDown(e); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3f1603eabe..44be73b089 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -80,11 +80,18 @@ namespace osu.Game.Screens.Play protected GameplayClockContainer GameplayClockContainer { get; private set; } protected DimmableStoryboard DimmableStoryboard { get; private set; } + protected DimmableVideo DimmableVideo { get; private set; } [Cached] [Cached(Type = typeof(IBindable>))] protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); + /// + /// Whether failing should be allowed. + /// By default, this checks whether all selected mods allow failing. + /// + protected virtual bool AllowFail => Mods.Value.OfType().All(m => m.AllowFail); + private readonly bool allowPause; private readonly bool showResults; @@ -142,6 +149,7 @@ namespace osu.Game.Screens.Play private void addUnderlayComponents(Container target) { + target.Add(DimmableVideo = new DimmableVideo(Beatmap.Value.Video) { RelativeSizeAxes = Axes.Both }); target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }); } @@ -358,7 +366,7 @@ namespace osu.Game.Screens.Play private bool onFail() { - if (Mods.Value.OfType().Any(m => !m.AllowFail)) + if (!AllowFail) return false; HasFailed = true; @@ -370,6 +378,10 @@ namespace osu.Game.Screens.Play PauseOverlay.Hide(); failAnimation.Start(); + + if (Mods.Value.OfType().Any(m => m.RestartOnFail)) + Restart(); + return true; } @@ -502,15 +514,18 @@ namespace osu.Game.Screens.Play return true; } - if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value) - // still want to block if we are within the cooldown period and not already paused. - return true; - - if (HasFailed && ValidForResume && !FailOverlay.IsPresent) - // ValidForResume is false when restarting + // ValidForResume is false when restarting + if (ValidForResume) { - failAnimation.FinishTransforms(true); - return true; + if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value) + // still want to block if we are within the cooldown period and not already paused. + return true; + + if (HasFailed && !FailOverlay.IsPresent) + { + failAnimation.FinishTransforms(true); + return true; + } } GameplayClockContainer.ResetLocalAdjustments(); diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index 1c8628f704..ff64f35a18 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -15,6 +15,7 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerSliderBar dimSliderBar; private readonly PlayerSliderBar blurSliderBar; private readonly PlayerCheckbox showStoryboardToggle; + private readonly PlayerCheckbox showVideoToggle; private readonly PlayerCheckbox beatmapSkinsToggle; private readonly PlayerCheckbox beatmapHitsoundsToggle; @@ -37,6 +38,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Text = "Toggles:" }, showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboards" }, + showVideoToggle = new PlayerCheckbox { LabelText = "Video" }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = "Beatmap skins" }, beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } }; @@ -48,6 +50,7 @@ namespace osu.Game.Screens.Play.PlayerSettings dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); + showVideoToggle.Current = config.GetBindable(OsuSetting.ShowVideoBackground); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index a9c0ee3a15..b040549efc 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -9,6 +9,9 @@ namespace osu.Game.Screens.Play { private readonly Score score; + // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) + protected override bool AllowFail => false; + public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true) : base(allowPause, showResults) { diff --git a/osu.Game/Screens/Play/SongProgressBar.cs b/osu.Game/Screens/Play/SongProgressBar.cs index dd7b5826d5..33c7595b37 100644 --- a/osu.Game/Screens/Play/SongProgressBar.cs +++ b/osu.Game/Screens/Play/SongProgressBar.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.MathUtils; +using osu.Framework.Threading; namespace osu.Game.Screens.Play { @@ -121,6 +122,12 @@ namespace osu.Game.Screens.Play handleBase.X = newX; } - protected override void OnUserChange(double value) => OnSeek?.Invoke(value); + private ScheduledDelegate scheduledSeek; + + protected override void OnUserChange(double value) + { + scheduledSeek?.Cancel(); + scheduledSeek = Schedule(() => OnSeek?.Invoke(value)); + } } } diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index 9c56725c4e..05f6128ac2 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -103,6 +103,7 @@ namespace osu.Game.Screens.Play var newColumns = new BufferedContainer { CacheDrawnFrameBuffer = true, + RedrawOnScale = false, RelativeSizeAxes = Axes.Both, }; diff --git a/osu.Game/Screens/Select/BeatmapDetailArea.cs b/osu.Game/Screens/Select/BeatmapDetailArea.cs index b66a2ffe0f..5348de68d6 100644 --- a/osu.Game/Screens/Select/BeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/BeatmapDetailArea.cs @@ -27,8 +27,8 @@ namespace osu.Game.Screens.Select set { beatmap = value; - Leaderboard.Beatmap = beatmap?.BeatmapInfo; Details.Beatmap = beatmap?.BeatmapInfo; + Leaderboard.Beatmap = beatmap is DummyWorkingBeatmap ? null : beatmap?.BeatmapInfo; } } diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs index 7f82d3cc12..6caef8e2aa 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Select { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Text = @"Mods", + Text = @"Selected Mods", Alpha = 0, }, }; diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 5f6307e3b4..65ecd7b812 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -154,6 +154,8 @@ namespace osu.Game.Screens.Select var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); CacheDrawnFrameBuffer = true; + RedrawOnScale = false; + RelativeSizeAxes = Axes.Both; titleBinding = localisation.GetLocalisedString(new LocalisedString((metadata.TitleUnicode, metadata.Title))); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 712ab7b571..6c3c9d20f3 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -24,12 +24,30 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - bool match = criteria.Ruleset == null || Beatmap.RulesetID == criteria.Ruleset.ID || (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); + bool match = + criteria.Ruleset == null || + Beatmap.RulesetID == criteria.Ruleset.ID || + (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); - foreach (var criteriaTerm in criteria.SearchTerms) - match &= - Beatmap.Metadata.SearchableTerms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) || - Beatmap.Version.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0; + match &= criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty); + match &= criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate); + match &= criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate); + match &= criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize); + match &= criteria.Length.IsInRange(Beatmap.Length); + match &= criteria.BPM.IsInRange(Beatmap.BPM); + + match &= criteria.BeatDivisor.IsInRange(Beatmap.BeatDivisor); + match &= criteria.OnlineStatus.IsInRange(Beatmap.Status); + + match &= criteria.Creator.Matches(Beatmap.Metadata.AuthorString); + match &= criteria.Artist.Matches(Beatmap.Metadata.Artist) || + criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode); + + if (match) + foreach (var criteriaTerm in criteria.SearchTerms) + match &= + Beatmap.Metadata.SearchableTerms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) || + Beatmap.Version.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0; Filtered.Value = !match; } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 97b6a78804..699e01bca7 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -146,6 +146,7 @@ namespace osu.Game.Screens.Select.Carousel public PanelBackground(WorkingBeatmap working) { CacheDrawnFrameBuffer = true; + RedrawOnScale = false; Children = new Drawable[] { diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index ed74b01fc9..91f1ca0307 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -33,14 +33,21 @@ namespace osu.Game.Screens.Select private Bindable groupMode; - public FilterCriteria CreateCriteria() => new FilterCriteria + public FilterCriteria CreateCriteria() { - Group = groupMode.Value, - Sort = sortMode.Value, - SearchText = searchTextBox.Text, - AllowConvertedBeatmaps = showConverted.Value, - Ruleset = ruleset.Value - }; + var query = searchTextBox.Text; + + var criteria = new FilterCriteria + { + Group = groupMode.Value, + Sort = sortMode.Value, + AllowConvertedBeatmaps = showConverted.Value, + Ruleset = ruleset.Value + }; + + FilterQueryParser.ApplyQueries(criteria, query); + return criteria; + } public Action Exit; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 140010ff54..c2cbac905e 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Screens.Select.Filter; @@ -13,6 +15,17 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; + public OptionalRange StarDifficulty; + public OptionalRange ApproachRate; + public OptionalRange DrainRate; + public OptionalRange CircleSize; + public OptionalRange Length; + public OptionalRange BPM; + public OptionalRange BeatDivisor; + public OptionalRange OnlineStatus; + public OptionalTextFilter Creator; + public OptionalTextFilter Artist; + public string[] SearchTerms = Array.Empty(); public RulesetInfo Ruleset; @@ -26,8 +39,69 @@ namespace osu.Game.Screens.Select set { searchText = value; - SearchTerms = searchText.Split(',', ' ', '!').Where(s => !string.IsNullOrEmpty(s)).ToArray(); + SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray(); } } + + public struct OptionalRange : IEquatable> + where T : struct, IComparable + { + public bool IsInRange(T value) + { + if (Min != null) + { + int comparison = Comparer.Default.Compare(value, Min.Value); + + if (comparison < 0) + return false; + + if (comparison == 0 && !IsLowerInclusive) + return false; + } + + if (Max != null) + { + int comparison = Comparer.Default.Compare(value, Max.Value); + + if (comparison > 0) + return false; + + if (comparison == 0 && !IsUpperInclusive) + return false; + } + + return true; + } + + public T? Min; + public T? Max; + public bool IsLowerInclusive; + public bool IsUpperInclusive; + + public bool Equals(OptionalRange other) + => Min.Equals(other.Min) + && Max.Equals(other.Max) + && IsLowerInclusive.Equals(other.IsLowerInclusive) + && IsUpperInclusive.Equals(other.IsUpperInclusive); + } + + public struct OptionalTextFilter : IEquatable + { + public bool Matches(string value) + { + if (string.IsNullOrEmpty(SearchTerm)) + return true; + + // search term is guaranteed to be non-empty, so if the string we're comparing is empty, it's not matching + if (string.IsNullOrEmpty(value)) + return false; + + return value.IndexOf(SearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0; + } + + public string SearchTerm; + + public bool Equals(OptionalTextFilter other) => SearchTerm?.Equals(other.SearchTerm) ?? true; + } } } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs new file mode 100644 index 0000000000..ffe1258168 --- /dev/null +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -0,0 +1,211 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Globalization; +using System.Text.RegularExpressions; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.Select +{ + internal static class FilterQueryParser + { + private static readonly Regex query_syntax_regex = new Regex( + @"\b(?stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + internal static void ApplyQueries(FilterCriteria criteria, string query) + { + foreach (Match match in query_syntax_regex.Matches(query)) + { + var key = match.Groups["key"].Value.ToLower(); + var op = match.Groups["op"].Value; + var value = match.Groups["value"].Value; + + parseKeywordCriteria(criteria, key, value, op); + + query = query.Replace(match.ToString(), ""); + } + + criteria.SearchText = query; + } + + private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op) + { + switch (key) + { + case "stars" when parseFloatWithPoint(value, out var stars): + updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2); + break; + + case "ar" when parseFloatWithPoint(value, out var ar): + updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2); + break; + + case "dr" when parseFloatWithPoint(value, out var dr): + updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); + break; + + case "cs" when parseFloatWithPoint(value, out var cs): + updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2); + break; + + case "bpm" when parseDoubleWithPoint(value, out var bpm): + updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2); + break; + + case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length): + var scale = getLengthScale(value); + updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0); + break; + + case "divisor" when parseInt(value, out var divisor): + updateCriteriaRange(ref criteria.BeatDivisor, op, divisor); + break; + + case "status" when Enum.TryParse(value, true, out var statusValue): + updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue); + break; + + case "creator": + updateCriteriaText(ref criteria.Creator, op, value); + break; + + case "artist": + updateCriteriaText(ref criteria.Artist, op, value); + break; + } + } + + private static int getLengthScale(string value) => + value.EndsWith("ms") ? 1 : + value.EndsWith("s") ? 1000 : + value.EndsWith("m") ? 60000 : + value.EndsWith("h") ? 3600000 : 1000; + + private static bool parseFloatWithPoint(string value, out float result) => + float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result); + + private static bool parseDoubleWithPoint(string value, out double result) => + double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result); + + private static bool parseInt(string value, out int result) => + int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result); + + private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value) + { + switch (op) + { + case "=": + case ":": + textFilter.SearchTerm = value.Trim('"'); + break; + } + } + + private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, float value, float tolerance = 0.05f) + { + switch (op) + { + default: + return; + + case "=": + case ":": + range.Min = value - tolerance; + range.Max = value + tolerance; + break; + + case ">": + range.Min = value + tolerance; + break; + + case ">=": + case ">:": + range.Min = value - tolerance; + break; + + case "<": + range.Max = value - tolerance; + break; + + case "<=": + case "<:": + range.Max = value + tolerance; + break; + } + } + + private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, double value, double tolerance = 0.05) + { + switch (op) + { + default: + return; + + case "=": + case ":": + range.Min = value - tolerance; + range.Max = value + tolerance; + break; + + case ">": + range.Min = value + tolerance; + break; + + case ">=": + case ">:": + range.Min = value - tolerance; + break; + + case "<": + range.Max = value - tolerance; + break; + + case "<=": + case "<:": + range.Max = value + tolerance; + break; + } + } + + private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, T value) + where T : struct, IComparable + { + switch (op) + { + default: + return; + + case "=": + case ":": + range.IsLowerInclusive = range.IsUpperInclusive = true; + range.Min = value; + range.Max = value; + break; + + case ">": + range.IsLowerInclusive = false; + range.Min = value; + break; + + case ">=": + case ">:": + range.IsLowerInclusive = true; + range.Min = value; + break; + + case "<": + range.IsUpperInclusive = false; + range.Max = value; + break; + + case "<=": + case "<:": + range.IsUpperInclusive = true; + range.Max = value; + break; + } + } + } +} diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index cb45c00f66..337d46ecdd 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -37,8 +38,25 @@ namespace osu.Game.Screens.Select.Leaderboards } } + public APILegacyUserTopScoreInfo TopScore + { + get => topScoreContainer.Score.Value; + set + { + if (value == null) + topScoreContainer.Hide(); + else + { + topScoreContainer.Show(); + topScoreContainer.Score.Value = value; + } + } + } + private bool filterMods; + private UserTopScoreContainer topScoreContainer; + /// /// Whether to apply the game's currently selected mods as a filter when retrieving scores. /// @@ -77,12 +95,29 @@ namespace osu.Game.Screens.Select.Leaderboards if (filterMods) UpdateScores(); }; + + Content.Add(topScoreContainer = new UserTopScoreContainer + { + ScoreSelected = s => ScoreSelected?.Invoke(s) + }); + } + + protected override void Reset() + { + base.Reset(); + TopScore = null; } protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; protected override APIRequest FetchScores(Action> scoresCallback) { + if (Beatmap == null) + { + PlaceholderState = PlaceholderState.NoneSelected; + return null; + } + if (Scope == BeatmapLeaderboardScope.Local) { var scores = scoreManager @@ -113,7 +148,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (Beatmap?.OnlineBeatmapID == null || Beatmap?.Status <= BeatmapSetOnlineStatus.Pending) + if (Beatmap.OnlineBeatmapID == null || Beatmap?.Status <= BeatmapSetOnlineStatus.Pending) { PlaceholderState = PlaceholderState.Unavailable; return null; @@ -135,7 +170,11 @@ namespace osu.Game.Screens.Select.Leaderboards var req = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope, requestMods); - req.Success += r => scoresCallback?.Invoke(r.Scores); + req.Success += r => + { + scoresCallback?.Invoke(r.Scores); + TopScore = r.UserScore; + }; return req; } diff --git a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs new file mode 100644 index 0000000000..da8f676cd0 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs @@ -0,0 +1,94 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Threading; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public class UserTopScoreContainer : VisibilityContainer + { + private const int duration = 500; + + private readonly Container scoreContainer; + + public Bindable Score = new Bindable(); + + public Action ScoreSelected; + + protected override bool StartHidden => true; + + public UserTopScoreContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Margin = new MarginPadding { Vertical = 5 }; + + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = @"your personal best".ToUpper(), + Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), + }, + scoreContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + } + } + }; + + Score.BindValueChanged(onScoreChanged); + } + + private CancellationTokenSource loadScoreCancellation; + + private void onScoreChanged(ValueChangedEvent score) + { + var newScore = score.NewValue; + + scoreContainer.Clear(); + loadScoreCancellation?.Cancel(); + + if (newScore == null) + return; + + LoadComponentAsync(new LeaderboardScore(newScore.Score, newScore.Position) + { + Action = () => ScoreSelected?.Invoke(newScore.Score) + }, drawableScore => + { + scoreContainer.Child = drawableScore; + drawableScore.FadeInFromZero(duration, Easing.OutQuint); + }, (loadScoreCancellation = new CancellationTokenSource()).Token); + } + + protected override void PopIn() => this.FadeIn(duration, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(duration, Easing.OutQuint); + } +} diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index d0cb5986a8..fca801ce78 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -223,7 +223,7 @@ namespace osu.Game.Screens.Select }); } - BeatmapDetails.Leaderboard.ScoreSelected += s => this.Push(new SoloResults(s)); + BeatmapDetails.Leaderboard.ScoreSelected += score => this.Push(new SoloResults(score)); } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index b35c9c7b97..4b6eea6b6e 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -3,6 +3,7 @@ using osu.Framework.Audio; using osu.Framework.IO.Stores; +using osuTK.Graphics; namespace osu.Game.Skinning { @@ -11,6 +12,14 @@ namespace osu.Game.Skinning public DefaultLegacySkin(IResourceStore storage, AudioManager audioManager) : base(Info, storage, audioManager, string.Empty) { + Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); + Configuration.ComboColours.AddRange(new[] + { + new Color4(255, 192, 0, 255), + new Color4(0, 202, 0, 255), + new Color4(18, 124, 255, 255), + new Color4(242, 24, 57, 255), + }); } public static SkinInfo Info { get; } = new SkinInfo diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 9eda5d597a..529c1afca5 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -1,10 +1,13 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osuTK.Graphics; namespace osu.Game.Skinning { @@ -21,5 +24,24 @@ namespace osu.Game.Skinning public override Texture GetTexture(string componentName) => null; public override SampleChannel GetSample(ISampleInfo sampleInfo) => null; + + public override IBindable GetConfig(TLookup lookup) + { + switch (lookup) + { + // todo: this code is pulled from LegacySkin and should not exist. + // will likely change based on how databased storage of skin configuration goes. + case GlobalSkinConfiguration global: + switch (global) + { + case GlobalSkinConfiguration.ComboColours: + return SkinUtils.As(new Bindable>(Configuration.ComboColours)); + } + + break; + } + + return null; + } } } diff --git a/osu.Game/Skinning/DefaultSkinConfiguration.cs b/osu.Game/Skinning/DefaultSkinConfiguration.cs index 722b35f102..f52fac6077 100644 --- a/osu.Game/Skinning/DefaultSkinConfiguration.cs +++ b/osu.Game/Skinning/DefaultSkinConfiguration.cs @@ -12,8 +12,6 @@ namespace osu.Game.Skinning { public DefaultSkinConfiguration() { - HitCircleFont = "default"; - ComboColours.AddRange(new[] { new Color4(17, 136, 170, 255), @@ -21,8 +19,6 @@ namespace osu.Game.Skinning new Color4(204, 102, 0, 255), new Color4(121, 9, 13, 255) }); - - CursorExpand = true; } } } diff --git a/osu.Game/Skinning/GameplaySkinComponent.cs b/osu.Game/Skinning/GameplaySkinComponent.cs index 8695b3d720..2aa380fa90 100644 --- a/osu.Game/Skinning/GameplaySkinComponent.cs +++ b/osu.Game/Skinning/GameplaySkinComponent.cs @@ -5,7 +5,7 @@ using System.Linq; namespace osu.Game.Skinning { - public class GameplaySkinComponent : ISkinComponent where T : struct + public class GameplaySkinComponent : ISkinComponent { public readonly T Component; diff --git a/osu.Game/Skinning/GlobalSkinColour.cs b/osu.Game/Skinning/GlobalSkinColour.cs new file mode 100644 index 0000000000..d039be98ce --- /dev/null +++ b/osu.Game/Skinning/GlobalSkinColour.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Skinning +{ + public enum GlobalSkinColour + { + MenuGlow + } +} diff --git a/osu.Game/Skinning/GlobalSkinConfiguration.cs b/osu.Game/Skinning/GlobalSkinConfiguration.cs new file mode 100644 index 0000000000..66dc9a9395 --- /dev/null +++ b/osu.Game/Skinning/GlobalSkinConfiguration.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Skinning +{ + public enum GlobalSkinConfiguration + { + ComboColours + } +} diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index bc1ae634c9..cb2a379b8e 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -1,8 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; +using JetBrains.Annotations; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Audio; @@ -14,12 +15,36 @@ namespace osu.Game.Skinning /// public interface ISkin { + /// + /// Retrieve a component implementation. + /// + /// The requested component. + /// A drawable representation for the requested component, or null if unavailable. + [CanBeNull] Drawable GetDrawableComponent(ISkinComponent component); + /// + /// Retrieve a . + /// + /// The requested texture. + /// A matching texture, or null if unavailable. + [CanBeNull] Texture GetTexture(string componentName); + /// + /// Retrieve a . + /// + /// The requested sample. + /// A matching sample channel, or null if unavailable. + [CanBeNull] SampleChannel GetSample(ISampleInfo sampleInfo); - TValue GetValue(Func query) where TConfiguration : SkinConfiguration; + /// + /// Retrieve a configuration value. + /// + /// The requested configuration value. + /// A matching value boxed in an , or null if unavailable. + [CanBeNull] + IBindable GetConfig(TLookup lookup); } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 535471f455..0b1076be01 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -1,10 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.IO; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; @@ -16,29 +19,32 @@ namespace osu.Game.Skinning { public class LegacySkin : Skin { + [CanBeNull] protected TextureStore Textures; + [CanBeNull] protected IResourceStore Samples; public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) : this(skin, new LegacySkinResourceStore(skin, storage), audioManager, "skin.ini") { - // defaults should only be applied for non-beatmap skins (which are parsed via this constructor). - if (!Configuration.CustomColours.ContainsKey("SliderBall")) Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); } protected LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, string filename) : base(skin) { - Stream stream = storage.GetStream(filename); + Stream stream = storage?.GetStream(filename); if (stream != null) using (StreamReader reader = new StreamReader(stream)) Configuration = new LegacySkinDecoder().Decode(reader); else Configuration = new DefaultSkinConfiguration(); - Samples = audioManager.GetSampleStore(storage); - Textures = new TextureStore(new TextureLoaderStore(storage)); + if (storage != null) + { + Samples = audioManager?.GetSampleStore(storage); + Textures = new TextureStore(new TextureLoaderStore(storage)); + } } protected override void Dispose(bool isDisposing) @@ -48,6 +54,52 @@ namespace osu.Game.Skinning Samples?.Dispose(); } + public override IBindable GetConfig(TLookup lookup) + { + switch (lookup) + { + case GlobalSkinConfiguration global: + switch (global) + { + case GlobalSkinConfiguration.ComboColours: + return SkinUtils.As(new Bindable>(Configuration.ComboColours)); + } + + break; + + case GlobalSkinColour colour: + return SkinUtils.As(getCustomColour(colour.ToString())); + + case SkinCustomColourLookup customColour: + return SkinUtils.As(getCustomColour(customColour.Lookup.ToString())); + + default: + try + { + if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out var val)) + { + // special case for handling skins which use 1 or 0 to signify a boolean state. + if (typeof(TValue) == typeof(bool)) + val = val == "1" ? "true" : "false"; + + var bindable = new Bindable(); + if (val != null) + bindable.Parse(val); + return bindable; + } + } + catch + { + } + + break; + } + + return null; + } + + private IBindable getCustomColour(string lookup) => Configuration.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null; + public override Drawable GetDrawableComponent(ISkinComponent component) { switch (component) @@ -79,12 +131,12 @@ namespace osu.Game.Skinning componentName = getFallbackName(componentName); float ratio = 2; - var texture = Textures.Get($"{componentName}@2x"); + var texture = Textures?.Get($"{componentName}@2x"); if (texture == null) { ratio = 1; - texture = Textures.Get(componentName); + texture = Textures?.Get(componentName); } if (texture != null) @@ -97,7 +149,7 @@ namespace osu.Game.Skinning { foreach (var lookup in sampleInfo.LookupNames) { - var sample = Samples.Get(getFallbackName(lookup)); + var sample = Samples?.Get(getFallbackName(lookup)); if (sample != null) return sample; @@ -105,7 +157,7 @@ namespace osu.Game.Skinning if (sampleInfo is HitSampleInfo hsi) // Try fallback to non-bank samples. - return Samples.Get(hsi.Name); + return Samples?.Get(hsi.Name); return null; } diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs index 0160755eed..e97664e75e 100644 --- a/osu.Game/Skinning/LegacySkinDecoder.cs +++ b/osu.Game/Skinning/LegacySkinDecoder.cs @@ -14,47 +14,31 @@ namespace osu.Game.Skinning protected override void ParseLine(DefaultSkinConfiguration skin, Section section, string line) { - line = StripComments(line); - - var pair = SplitKeyVal(line); - - switch (section) + if (section != Section.Colours) { - case Section.General: - switch (pair.Key) - { - case @"Name": - skin.SkinInfo.Name = pair.Value; - break; + line = StripComments(line); - case @"Author": - skin.SkinInfo.Creator = pair.Value; - break; + var pair = SplitKeyVal(line); - case @"CursorExpand": - skin.CursorExpand = pair.Value != "0"; - break; + switch (section) + { + case Section.General: + switch (pair.Key) + { + case @"Name": + skin.SkinInfo.Name = pair.Value; + return; - case @"SliderBorderSize": - skin.SliderBorderSize = Parsing.ParseFloat(pair.Value); - break; - } + case @"Author": + skin.SkinInfo.Creator = pair.Value; + return; + } - break; + break; + } - case Section.Fonts: - switch (pair.Key) - { - case "HitCirclePrefix": - skin.HitCircleFont = pair.Value; - break; - - case "HitCircleOverlap": - skin.HitCircleOverlap = int.Parse(pair.Value); - break; - } - - break; + if (!string.IsNullOrEmpty(pair.Key)) + skin.ConfigDictionary[pair.Key] = pair.Value; } base.ParseLine(skin, section, line); diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index dbcec019d6..773a9dc5c6 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using osu.Framework.Graphics.Sprites; using osu.Framework.Text; -using osu.Game.Graphics; using osu.Game.Graphics.Sprites; namespace osu.Game.Skinning @@ -18,7 +17,7 @@ namespace osu.Game.Skinning Shadow = false; UseFullGlyphHeight = false; - Font = new FontUsage(font, OsuFont.DEFAULT_FONT_SIZE); + Font = new FontUsage(font, 1); glyphStore = new LegacyGlyphStore(skin); } @@ -37,10 +36,6 @@ namespace osu.Game.Skinning { var texture = skin.GetTexture($"{fontName}-{character}"); - if (texture != null) - // Approximate value that brings character sizing roughly in-line with stable - texture.ScaleAdjust *= 18; - if (texture == null) return null; diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 299f257e57..fa4aebd8a5 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -1,8 +1,9 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Audio; @@ -13,7 +14,7 @@ namespace osu.Game.Skinning { public readonly SkinInfo SkinInfo; - public virtual SkinConfiguration Configuration { get; protected set; } + public SkinConfiguration Configuration { get; protected set; } public abstract Drawable GetDrawableComponent(ISkinComponent componentName); @@ -21,8 +22,7 @@ namespace osu.Game.Skinning public abstract Texture GetTexture(string componentName); - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration - => Configuration is TConfiguration conf ? query.Invoke(conf) : default; + public abstract IBindable GetConfig(TLookup lookup); protected Skin(SkinInfo skin) { diff --git a/osu.Game/Skinning/SkinConfigManager.cs b/osu.Game/Skinning/SkinConfigManager.cs new file mode 100644 index 0000000000..896444d1d2 --- /dev/null +++ b/osu.Game/Skinning/SkinConfigManager.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Configuration; + +namespace osu.Game.Skinning +{ + public class SkinConfigManager : ConfigManager where T : struct + { + protected override void PerformLoad() + { + } + + protected override bool PerformSave() => false; + } +} diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index d585c58ef1..54aac86e3c 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -18,14 +18,6 @@ namespace osu.Game.Skinning public Dictionary CustomColours { get; set; } = new Dictionary(); - public string HitCircleFont { get; set; } - - public int HitCircleOverlap { get; set; } - - public float? SliderBorderSize { get; set; } - - public float? SliderPathRadius { get; set; } - - public bool? CursorExpand { get; set; } + public readonly Dictionary ConfigDictionary = new Dictionary(); } } diff --git a/osu.Game/Skinning/SkinCustomColourLookup.cs b/osu.Game/Skinning/SkinCustomColourLookup.cs new file mode 100644 index 0000000000..b8e5ac9b53 --- /dev/null +++ b/osu.Game/Skinning/SkinCustomColourLookup.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Skinning +{ + public class SkinCustomColourLookup + { + public readonly object Lookup; + + public SkinCustomColourLookup(object lookup) + { + Lookup = lookup; + } + } +} diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index a55a128dff..aa3b3981c2 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -131,6 +131,6 @@ namespace osu.Game.Skinning public SampleChannel GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => CurrentSkin.Value.GetValue(query); + public IBindable GetConfig(TLookup lookup) => CurrentSkin.Value.GetConfig(lookup); } } diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 85a80655ea..ef7f5f381b 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; @@ -64,13 +65,16 @@ namespace osu.Game.Skinning return fallbackSource?.GetSample(sampleInfo); } - public TValue GetValue(Func query) where TConfiguration : SkinConfiguration + public IBindable GetConfig(TLookup lookup) { - TValue val; - if (AllowConfigurationLookup && skin != null && (val = skin.GetValue(query)) != null) - return val; + if (AllowConfigurationLookup && skin != null) + { + var bindable = skin.GetConfig(lookup); + if (bindable != null) + return bindable; + } - return fallbackSource == null ? default : fallbackSource.GetValue(query); + return fallbackSource?.GetConfig(lookup); } protected virtual void TriggerSourceChanged() => SourceChanged?.Invoke(); diff --git a/osu.Game/Skinning/SkinUtils.cs b/osu.Game/Skinning/SkinUtils.cs new file mode 100644 index 0000000000..e3bc5e28b8 --- /dev/null +++ b/osu.Game/Skinning/SkinUtils.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; + +namespace osu.Game.Skinning +{ + /// + /// Contains helper methods to assist in implementing s. + /// + public static class SkinUtils + { + /// + /// Converts an to a . Used for returning configuration values of specific types. + /// + /// The value. + /// The type of value , and the type of the resulting bindable. + /// The resulting bindable. + public static Bindable As(object value) => (Bindable)value; + } +} diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 3d0219ed93..bdf8be773b 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Audio; @@ -20,7 +21,7 @@ namespace osu.Game.Skinning private SampleChannel[] channels; - private AudioManager audio; + private ISampleStore samples; public SkinnableSound(IEnumerable hitSamples) { @@ -33,9 +34,9 @@ namespace osu.Game.Skinning } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(ISampleStore samples) { - this.audio = audio; + this.samples = samples; } private bool looping; @@ -81,7 +82,7 @@ namespace osu.Game.Skinning if (ch == null && allowFallback) foreach (var lookup in s.LookupNames) - if ((ch = audio.Samples.Get($"Gameplay/{lookup}")) != null) + if ((ch = samples.Get($"Gameplay/{lookup}")) != null) break; if (ch != null) @@ -102,8 +103,9 @@ namespace osu.Game.Skinning { base.Dispose(isDisposing); - foreach (var c in channels) - c.Dispose(); + if (channels != null) + foreach (var c in channels) + c.Dispose(); } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index b04f1d4518..f3f8308964 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -64,5 +64,11 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = sampleInfo.StartTime; } } + + protected override void Dispose(bool isDisposing) + { + channel?.Stop(); + base.Dispose(isDisposing); + } } } diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index a555a52e42..3fc9662b17 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets; @@ -201,6 +202,8 @@ namespace osu.Game.Tests.Beatmaps protected override Texture GetBackground() => throw new NotImplementedException(); + protected override VideoSprite GetVideo() => throw new NotImplementedException(); + protected override Track GetTrack() => throw new NotImplementedException(); protected override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs new file mode 100644 index 0000000000..e9251f8011 --- /dev/null +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets; + +namespace osu.Game.Tests.Beatmaps +{ + /// + /// Base class for tests of converting enumeration flags to ruleset mod instances. + /// + public abstract class LegacyModConversionTest + { + /// + /// Creates the whose legacy mod conversion is to be tested. + /// + /// + protected abstract Ruleset CreateRuleset(); + + protected void Test(LegacyMods legacyMods, Type[] expectedMods) + { + var ruleset = CreateRuleset(); + var mods = ruleset.ConvertLegacyMods(legacyMods).ToList(); + Assert.AreEqual(expectedMods.Length, mods.Count); + + foreach (var modType in expectedMods) + { + Assert.IsNotNull(mods.SingleOrDefault(mod => mod.GetType() == modType)); + } + } + } +} diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index 0ef35879e3..0d9f4f51be 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -3,6 +3,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; using osu.Game.Beatmaps; namespace osu.Game.Tests.Beatmaps @@ -25,6 +26,8 @@ namespace osu.Game.Tests.Beatmaps protected override Texture GetBackground() => null; + protected override VideoSprite GetVideo() => null; + protected override Track GetTrack() => null; } } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 27d72f3950..8e98d51962 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -16,6 +16,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Tests.Beatmaps; @@ -45,8 +46,28 @@ namespace osu.Game.Tests.Visual protected Storage LocalStorage => localStorage.Value; private readonly Lazy contextFactory; + + protected IAPIProvider API + { + get + { + if (UseOnlineAPI) + throw new InvalidOperationException($"Using the {nameof(OsuTestScene)} dummy API is not supported when {nameof(UseOnlineAPI)} is true"); + + return dummyAPI; + } + } + + private DummyAPIAccess dummyAPI; + protected DatabaseContextFactory ContextFactory => contextFactory.Value; + /// + /// Whether this test scene requires real-world API access. + /// If true, this will bypass the local and use the provided one. + /// + protected virtual bool UseOnlineAPI => false; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { // This is the earliest we can get OsuGameBase, which is used by the dummy working beatmap to find textures @@ -57,7 +78,16 @@ namespace osu.Game.Tests.Visual Default = working }; - return Dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + Dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + if (!UseOnlineAPI) + { + dummyAPI = new DummyAPIAccess(); + Dependencies.CacheAs(dummyAPI); + Add(dummyAPI); + } + + return Dependencies; } protected OsuTestScene() diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index ccd996098c..2c5a51ca02 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -50,7 +50,11 @@ namespace osu.Game.Tests.Visual Beatmap.Value = CreateWorkingBeatmap(beatmap); if (!AllowFail) - Mods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; + { + var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail); + if (noFailMod != null) + Mods.Value = new[] { noFailMod }; + } if (Autoplay) { diff --git a/osu.Game/Users/Drawables/DrawableFlag.cs b/osu.Game/Users/Drawables/DrawableFlag.cs index 368354e48e..1d648e46b6 100644 --- a/osu.Game/Users/Drawables/DrawableFlag.cs +++ b/osu.Game/Users/Drawables/DrawableFlag.cs @@ -26,7 +26,7 @@ namespace osu.Game.Users.Drawables if (ts == null) throw new ArgumentNullException(nameof(ts)); - Texture = ts.Get($@"Flags/{country?.FlagName ?? @"__"}"); + Texture = ts.Get($@"Flags/{country?.FlagName ?? @"__"}") ?? ts.Get(@"Flags/__"); } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ca69bb2295..83632f3d41 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -6,19 +6,30 @@ AnyCPU true + + osu! + ppy.osu.Game + ppy Pty Ltd + https://github.com/ppy/osu/blob/master/LICENCE.md + https://github.com/ppy/osu + https://github.com/ppy/osu + Automated release. + Copyright (c) 2019 ppy Pty Ltd + osu game + - + - - + + - + diff --git a/osu.iOS.props b/osu.iOS.props index 86a2a40940..30f1da362d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -113,17 +113,17 @@ - + - - - + + + - + diff --git a/osu.iOS.sln.DotSettings b/osu.iOS.sln.DotSettings index 3b2b851d45..752b817910 100644 --- a/osu.iOS.sln.DotSettings +++ b/osu.iOS.sln.DotSettings @@ -1,4 +1,4 @@ - + True True True @@ -165,8 +165,17 @@ HINT HINT WARNING + WARNING <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile> Code Cleanup (peppy) + Required + Required + Required + Explicit + ExpressionBody + ExpressionBody + True + NEXT_LINE True True True @@ -176,12 +185,22 @@ True True NEXT_LINE + 1 + 1 + NEXT_LINE + MULTILINE NEXT_LINE + 1 + 1 True + NEXT_LINE NEVER NEVER + True False + True NEVER + False False True False @@ -189,6 +208,7 @@ True True False + False CHOP_IF_LONG True 200 diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index c3e274569d..ed162eed6e 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -218,9 +218,14 @@ WARNING <?xml version="1.0" encoding="utf-16"?><Profile name="Code Cleanup (peppy)"><CSArrangeThisQualifier>True</CSArrangeThisQualifier><CSUseVar><BehavourStyle>CAN_CHANGE_TO_EXPLICIT</BehavourStyle><LocalVariableStyle>ALWAYS_EXPLICIT</LocalVariableStyle><ForeachVariableStyle>ALWAYS_EXPLICIT</ForeachVariableStyle></CSUseVar><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSUpdateFileHeader>True</CSUpdateFileHeader><CSCodeStyleAttributes ArrangeTypeAccessModifier="False" ArrangeTypeMemberAccessModifier="False" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="False" ArrangeBraces="False" ArrangeAttributes="False" ArrangeArgumentsStyle="False" /><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSArrangeQualifiers>True</CSArrangeQualifiers></Profile> Code Cleanup (peppy) + Required + Required + Required + Explicit ExpressionBody ExpressionBody True + NEXT_LINE True True True @@ -232,14 +237,20 @@ NEXT_LINE 1 1 + NEXT_LINE + MULTILINE NEXT_LINE 1 1 True + NEXT_LINE NEVER NEVER + True False + True NEVER + False False True False @@ -247,6 +258,7 @@ True True False + False CHOP_IF_LONG True 200