diff --git a/README.md b/README.md index a1f478e39a..dc36145337 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ Clone the repository including submodules Build and run - Using Visual Studio 2017, Rider or Visual Studio Code (configurations are included) -- From command line using `dotnet run --project osu.Desktop` +- From command line using `dotnet run --project osu.Desktop`. When building for non-development purposes, add `-c Release` to gain higher performance. + +Note: If you run from command line under linux, you will need to prefix the output folder to your `LD_LIBRARY_PATH`. See `.vscode/launch.json` for an example If you run into issues building you may need to restore nuget packages (commonly via `dotnet restore`). Visual Studio Code users must run `Restore` task from debug tab before attempt to build. diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index cc08e08653..257155478f 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -4,7 +4,11 @@ using System; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework; +using osu.Framework.Development; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IPC; @@ -20,6 +24,8 @@ namespace osu.Desktop using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) { + host.ExceptionThrown += handleException; + if (!host.IsPrimaryInstance) { var importer = new ArchiveImportIPCChannel(host); @@ -45,5 +51,24 @@ namespace osu.Desktop return 0; } } + + private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1; + + /// + /// Allow a maximum of one unhandled exception, per second of execution. + /// + /// + /// + private static bool handleException(Exception arg) + { + bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0; + + Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} ."); + + // restore the stock of allowable exceptions after a short delay. + Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions)); + + return continueExecution; + } } } diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 6ee9c3155e..e2fc4d14f6 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -27,9 +27,9 @@ - - - + + + diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 68a8dfb7d3..15d4edc411 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps RepeatCount = curveData.RepeatCount, X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH, NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0 }; } @@ -51,7 +52,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps StartTime = obj.StartTime, Samples = obj.Samples, Duration = endTime.Duration, - NewCombo = comboData?.NewCombo ?? false + NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, }; } else @@ -61,6 +63,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps StartTime = obj.StartTime, Samples = obj.Samples, NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH }; } diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs index c39e663d75..f38009263f 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Judgements @@ -9,8 +10,6 @@ namespace osu.Game.Rulesets.Catch.Judgements { public override bool AffectsCombo => false; - public override bool ShouldExplode => true; - protected override int NumericResultFor(HitResult result) { switch (result) @@ -32,5 +31,7 @@ namespace osu.Game.Rulesets.Catch.Judgements return 8; } } + + public override bool ShouldExplodeFor(JudgementResult result) => true; } } diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs index 51d7d3b5cd..8a51867899 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs @@ -23,21 +23,10 @@ namespace osu.Game.Rulesets.Catch.Judgements } /// - /// The base health increase for the result achieved. + /// Retrieves the numeric health increase of a . /// - public float HealthIncrease => HealthIncreaseFor(Result); - - /// - /// Whether fruit on the platter should explode or drop. - /// Note that this is only checked if the owning object is also - /// - public virtual bool ShouldExplode => IsHit; - - /// - /// Convert a to a base health increase. - /// - /// The value to convert. - /// The base health increase. + /// The to find the numeric health increase for. + /// The numeric health increase of . protected virtual float HealthIncreaseFor(HitResult result) { switch (result) @@ -48,5 +37,18 @@ namespace osu.Game.Rulesets.Catch.Judgements return 10.2f; } } + + /// + /// Retrieves the numeric health increase of a . + /// + /// The to find the numeric health increase for. + /// The numeric health increase of . + public float HealthIncreaseFor(JudgementResult result) => HealthIncreaseFor(result.Type); + + /// + /// Whether fruit on the platter should explode or drop. + /// Note that this is only checked if the owning object is also + /// + public virtual bool ShouldExplodeFor(JudgementResult result) => result.IsHit; } } diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index f7c60a7a47..e1af4c1075 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -1,10 +1,15 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Catch.Objects { public class Banana : Fruit { public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; + + public override Judgement CreateJudgement() => new CatchBananaJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index d55cdac115..621fc100c2 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects public virtual bool NewCombo { get; set; } + public int ComboOffset { get; set; } + public int IndexInCurrentCombo { get; set; } public int ComboIndex { get; set; } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs index dd027abbe0..8756a5178f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs @@ -1,8 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Rulesets.Catch.Judgements; - namespace osu.Game.Rulesets.Catch.Objects.Drawable { public class DrawableBanana : DrawableFruit @@ -11,7 +9,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable : base(h) { } - - protected override CatchJudgement CreateJudgement() => new CatchBananaJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs index f039504600..697fab85c9 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs @@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable AddNested(getVisualRepresentation?.Invoke(b)); } - protected override bool ProvidesJudgement => false; - protected override void AddNested(DrawableHitObject h) { ((DrawableCatchHitObject)h).CheckPosition = o => CheckPosition?.Invoke(o) ?? false; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index 6ce2e6a2ae..9e840301fd 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -5,7 +5,6 @@ using System; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -53,20 +52,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable public Func CheckPosition; - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (CheckPosition == null) return; - if (timeOffset >= 0) - { - var judgement = CreateJudgement(); - judgement.Result = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss; - AddJudgement(judgement); - } + if (timeOffset >= 0 && Result != null) + ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss); } - protected virtual CatchJudgement CreateJudgement() => new CatchJudgement(); - protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs index 11d5ed1f92..5c8a7c4a7c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces; using OpenTK; using OpenTK.Graphics; -using osu.Game.Rulesets.Catch.Judgements; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -24,8 +23,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Masking = false; } - protected override CatchJudgement CreateJudgement() => new CatchDropletJudgement(); - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs index 854b63edeb..e66852c5c2 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs @@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable AddNested(getVisualRepresentation?.Invoke(o)); } - protected override bool ProvidesJudgement => false; - protected override void AddNested(DrawableHitObject h) { var catchObject = (DrawableCatchHitObject)h; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs index 2232bb81a7..e0f02454c4 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs @@ -2,18 +2,15 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using OpenTK; -using osu.Game.Rulesets.Catch.Judgements; namespace osu.Game.Rulesets.Catch.Objects.Drawable { public class DrawableTinyDroplet : DrawableDroplet { - public DrawableTinyDroplet(Droplet h) + public DrawableTinyDroplet(TinyDroplet h) : base(h) { Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS) / 8; } - - protected override CatchJudgement CreateJudgement() => new CatchTinyDropletJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs index f91a70c506..8b54922959 100644 --- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs @@ -1,9 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Catch.Objects { public class Droplet : CatchHitObject { + public override Judgement CreateJudgement() => new CatchDropletJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs index fcbb339ffd..2c2cd013c3 100644 --- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs @@ -1,9 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Catch.Objects { public class Fruit : CatchHitObject { + public override Judgement CreateJudgement() => new CatchJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs index 76cc8d9808..39f1cadad5 100644 --- a/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs @@ -1,9 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Catch.Objects { public class TinyDroplet : Droplet { + public override Judgement CreateJudgement() => new CatchTinyDropletJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 5b69d836a3..403cedde8c 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; @@ -21,55 +20,28 @@ namespace osu.Game.Rulesets.Catch.Scoring private float hpDrainRate; - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void ApplyBeatmap(Beatmap beatmap) { - hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; + base.ApplyBeatmap(beatmap); - foreach (var obj in beatmap.HitObjects) - { - switch (obj) - { - case JuiceStream stream: - foreach (var nestedObject in stream.NestedHitObjects) - switch (nestedObject) - { - case TinyDroplet _: - AddJudgement(new CatchTinyDropletJudgement { Result = HitResult.Perfect }); - break; - case Droplet _: - AddJudgement(new CatchDropletJudgement { Result = HitResult.Perfect }); - break; - case Fruit _: - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); - break; - } - break; - case BananaShower shower: - foreach (var _ in shower.NestedHitObjects.Cast()) - AddJudgement(new CatchBananaJudgement { Result = HitResult.Perfect }); - break; - case Fruit _: - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); - break; - } - } + hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; } private const double harshness = 0.01; - protected override void OnNewJudgement(Judgement judgement) + protected override void ApplyResult(JudgementResult result) { - base.OnNewJudgement(judgement); + base.ApplyResult(result); - if (judgement.Result == HitResult.Miss) + if (result.Type == HitResult.Miss) { - if (!judgement.IsBonus) + if (!result.Judgement.IsBonus) Health.Value -= hpDrainRate * (harshness * 2); return; } - if (judgement is CatchJudgement catchJudgement) - Health.Value += Math.Max(catchJudgement.HealthIncrease - hpDrainRate, 0) * harshness; + if (result.Judgement is CatchJudgement catchJudgement) + Health.Value += Math.Max(catchJudgement.HealthIncreaseFor(result) - hpDrainRate, 0) * harshness; } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index ea3b6fb0e0..d49be69856 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.UI public override void Add(DrawableHitObject h) { - h.OnJudgement += onJudgement; + h.OnNewResult += onNewResult; base.Add(h); @@ -67,6 +67,7 @@ namespace osu.Game.Rulesets.Catch.UI fruit.CheckPosition = CheckIfWeCanCatch; } - private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) => catcherArea.OnJudgement((DrawableCatchHitObject)judgedObject, judgement); + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) + => catcherArea.OnResult((DrawableCatchHitObject)judgedObject, result); } } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 7b06426b07..9460512a8d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.UI private DrawableCatchHitObject lastPlateableFruit; - public void OnJudgement(DrawableCatchHitObject fruit, Judgement judgement) + public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) { void runAfterLoaded(Action action) { @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.UI lastPlateableFruit.OnLoadComplete = _ => action(); } - if (judgement.IsHit && fruit.CanBePlated) + if (result.IsHit && fruit.CanBePlated) { var caughtFruit = (DrawableCatchHitObject)GetVisualRepresentation?.Invoke(fruit.HitObject); @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.UI if (fruit.HitObject.LastInCombo) { - if (((CatchJudgement)judgement).ShouldExplode) + if (((CatchJudgement)result.Judgement).ShouldExplodeFor(result)) runAfterLoaded(() => MovableCatcher.Explode()); else MovableCatcher.Drop(); @@ -407,9 +407,7 @@ namespace osu.Game.Rulesets.Catch.UI /// public void Explode() { - var fruit = caughtFruit.ToArray(); - - foreach (var f in fruit) + foreach (var f in caughtFruit.ToArray()) Explode(f); } @@ -422,15 +420,15 @@ namespace osu.Game.Rulesets.Catch.UI fruit.Anchor = Anchor.TopLeft; fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); - caughtFruit.Remove(fruit); + if (!caughtFruit.Remove(fruit)) + // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). + // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice. + return; ExplodingFruitTarget.Add(fruit); } - fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine) - .Then() - .MoveToY(fruit.Y + 50, 500, Easing.InSine); - + 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); diff --git a/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs index 5a93efb0dc..29663c2093 100644 --- a/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs @@ -14,24 +14,20 @@ namespace osu.Game.Rulesets.Mania.Tests /// public class ScrollingTestContainer : Container { - private readonly ScrollingDirection direction; + [Cached(Type = typeof(IScrollingInfo))] + private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); public ScrollingTestContainer(ScrollingDirection direction) { - this.direction = direction; + scrollingInfo.Direction.Value = direction; } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new ScrollingInfo { Direction = { Value = direction }}); - return dependencies; - } + public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up; + } - private class ScrollingInfo : IScrollingInfo - { - public readonly Bindable Direction = new Bindable(); - IBindable IScrollingInfo.Direction => Direction; - } + public class TestScrollingInfo : IScrollingInfo + { + public readonly Bindable Direction = new Bindable(); + IBindable IScrollingInfo.Direction => Direction; } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseEditor.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseEditor.cs new file mode 100644 index 0000000000..6c0f931cda --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseEditor.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class TestCaseEditor : EditorTestCase + { + private readonly Bindable direction = new Bindable(); + + public TestCaseEditor() + : base(new ManiaRuleset()) + { + AddStep("upwards scroll", () => direction.Value = ManiaScrollingDirection.Up); + AddStep("downwards scroll", () => direction.Value = ManiaScrollingDirection.Down); + } + + [BackgroundDependencyLoader] + private void load(RulesetConfigCache configCache) + { + var config = (ManiaConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance()); + config.BindWith(ManiaSetting.ScrollDirection, direction); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs index fdc8f362f7..a8b2b20fda 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs @@ -46,15 +46,20 @@ namespace osu.Game.Rulesets.Mania.Tests Spacing = new Vector2(20), Children = new[] { - createNoteDisplay(ScrollingDirection.Down), - createNoteDisplay(ScrollingDirection.Up), - createHoldNoteDisplay(ScrollingDirection.Down), - createHoldNoteDisplay(ScrollingDirection.Up), + createNoteDisplay(ScrollingDirection.Down, 1, out var note1), + createNoteDisplay(ScrollingDirection.Up, 2, out var note2), + createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1), + createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2), } }; + + AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2)); + AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0)); + AddAssert("hold note 1 facing downwards", () => verifyAnchors(holdNote1, Anchor.y2)); + AddAssert("hold note 2 facing upwards", () => verifyAnchors(holdNote2, Anchor.y0)); } - private Drawable createNoteDisplay(ScrollingDirection direction) + private Drawable createNoteDisplay(ScrollingDirection direction, int identifier, out DrawableNote hitObject) { var note = new Note { StartTime = 999999999 }; note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -62,24 +67,24 @@ namespace osu.Game.Rulesets.Mania.Tests return new ScrollingTestContainer(direction) { AutoSizeAxes = Axes.Both, - Child = new NoteContainer(direction, $"note, scrolling {direction.ToString().ToLowerInvariant()}") + Child = new NoteContainer(direction, $"note {identifier}, scrolling {direction.ToString().ToLowerInvariant()}") { - Child = new DrawableNote(note) { AccentColour = Color4.OrangeRed } + Child = hitObject = new DrawableNote(note) { AccentColour = Color4.OrangeRed } } }; } - private Drawable createHoldNoteDisplay(ScrollingDirection direction) + private Drawable createHoldNoteDisplay(ScrollingDirection direction, int identifier, out DrawableHoldNote hitObject) { - var note = new HoldNote { StartTime = 999999999, Duration = 1000 }; + var note = new HoldNote { StartTime = 999999999, Duration = 5000 }; note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); return new ScrollingTestContainer(direction) { AutoSizeAxes = Axes.Both, - Child = new NoteContainer(direction, $"hold note, scrolling {direction.ToString().ToLowerInvariant()}") + Child = new NoteContainer(direction, $"hold note {identifier}, scrolling {direction.ToString().ToLowerInvariant()}") { - Child = new DrawableHoldNote(note) + Child = hitObject = new DrawableHoldNote(note) { RelativeSizeAxes = Axes.Both, AccentColour = Color4.OrangeRed, @@ -88,6 +93,12 @@ namespace osu.Game.Rulesets.Mania.Tests }; } + private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor) + => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor); + + private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor) + => verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor)); + private class NoteContainer : Container { private readonly Container content; diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs index b1bb7f5187..5c5d955168 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Mania.Tests private readonly List stages = new List(); + private FillFlowContainer fill; + public TestCaseStage() : base(columns) { @@ -32,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests [BackgroundDependencyLoader] private void load() { - Child = new FillFlowContainer + Child = fill = new FillFlowContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -54,8 +57,22 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("hold note", createHoldNote); AddStep("minor bar line", () => createBarLine(false)); AddStep("major bar line", () => createBarLine(true)); + + AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.TopCentre)); + AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.BottomCentre)); + + AddStep("flip direction", () => + { + foreach (var c in fill.Children) + c.Flip(); + }); + + AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.BottomCentre)); + AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.TopCentre)); } + private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor); + private void createNote() { foreach (var stage in stages) @@ -101,7 +118,7 @@ namespace osu.Game.Rulesets.Mania.Tests } } - private Drawable createStage(ScrollingDirection direction, ManiaAction action) + private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action) { var specialAction = ManiaAction.Special1; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 8d0d78120a..37a8062d75 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -173,19 +173,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); int usableColumns = TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects; - int nextColumn = Random.Next(RandomStart, TotalColumns); + int nextColumn = GetRandomColumn(); for (int i = 0; i < Math.Min(usableColumns, noteCount); i++) { - while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn)) //find available column - nextColumn = Random.Next(RandomStart, TotalColumns); + // Find available column + nextColumn = FindAvailableColumn(nextColumn, pattern, PreviousPattern); addToPattern(pattern, nextColumn, startTime, EndTime); } // This is can't be combined with the above loop due to RNG for (int i = 0; i < noteCount - usableColumns; i++) { - while (pattern.ColumnHasObject(nextColumn)) - nextColumn = Random.Next(RandomStart, TotalColumns); + nextColumn = FindAvailableColumn(nextColumn, pattern); addToPattern(pattern, nextColumn, startTime, EndTime); } @@ -210,18 +209,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) - { - while (PreviousPattern.ColumnHasObject(nextColumn)) - nextColumn = Random.Next(RandomStart, TotalColumns); - } + nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); int lastColumn = nextColumn; for (int i = 0; i < noteCount; i++) { addToPattern(pattern, nextColumn, startTime, startTime); - while (nextColumn == lastColumn) - nextColumn = Random.Next(RandomStart, TotalColumns); - + nextColumn = FindAvailableColumn(nextColumn, validation: c => c != lastColumn); lastColumn = nextColumn; startTime += SegmentDuration; } @@ -313,7 +307,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (TotalColumns > 2) addToPattern(pattern, nextColumn, startTime, startTime); - nextColumn = Random.Next(RandomStart, TotalColumns); + nextColumn = GetRandomColumn(); startTime += SegmentDuration; } @@ -392,16 +386,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) - { - while (PreviousPattern.ColumnHasObject(nextColumn)) - nextColumn = Random.Next(RandomStart, TotalColumns); - } + nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); for (int i = 0; i < columnRepeat; i++) { - while (pattern.ColumnHasObject(nextColumn)) - nextColumn = Random.Next(RandomStart, TotalColumns); - + nextColumn = FindAvailableColumn(nextColumn, pattern); addToPattern(pattern, nextColumn, startTime, EndTime); startTime += SegmentDuration; } @@ -426,15 +415,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) - { - while (PreviousPattern.ColumnHasObject(holdColumn)) - holdColumn = Random.Next(RandomStart, TotalColumns); - } + holdColumn = FindAvailableColumn(holdColumn, PreviousPattern); // Create the hold note addToPattern(pattern, holdColumn, startTime, EndTime); - int nextColumn = Random.Next(RandomStart, TotalColumns); + int nextColumn = GetRandomColumn(); int noteCount; if (ConversionDifficulty > 6.5) noteCount = GetRandomNoteCount(0.63, 0); @@ -455,8 +441,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { for (int j = 0; j < noteCount; j++) { - while (rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn) - nextColumn = Random.Next(RandomStart, TotalColumns); + nextColumn = FindAvailableColumn(nextColumn, validation: c => c != holdColumn, patterns: rowPattern); addToPattern(rowPattern, nextColumn, startTime, startTime); } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 06b4b8a27e..775a4145e6 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -39,32 +39,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy addToPattern(pattern, 0, generateHold); break; case 8: - addToPattern(pattern, getNextRandomColumn(RandomStart), generateHold); + addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold); break; default: if (TotalColumns > 0) - addToPattern(pattern, getNextRandomColumn(0), generateHold); + addToPattern(pattern, GetRandomColumn(), generateHold); break; } return pattern; } - /// - /// Picks a random column after a column. - /// - /// The starting column. - /// A random column after . - private int getNextRandomColumn(int start) - { - int nextColumn = Random.Next(start, TotalColumns); - - while (PreviousPattern.ColumnHasObject(nextColumn)) - nextColumn = Random.Next(start, TotalColumns); - - return nextColumn; - } - /// /// Constructs and adds a note to a pattern. /// diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 84ebfdb839..da1dd62cf5 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -25,9 +25,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy PatternType lastStair, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { - if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime)); - if (density < 0) throw new ArgumentOutOfRangeException(nameof(density)); - StairType = lastStair; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); @@ -234,22 +231,27 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); for (int i = 0; i < noteCount; i++) { - while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking) - { - if (convertType.HasFlag(PatternType.Gathered)) - { - nextColumn++; - if (nextColumn == TotalColumns) - nextColumn = RandomStart; - } - else - nextColumn = Random.Next(RandomStart, TotalColumns); - } + nextColumn = allowStacking + ? FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: pattern) + : FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: new[] { pattern, PreviousPattern }); addToPattern(pattern, nextColumn); } return pattern; + + int getNextColumn(int last) + { + if (convertType.HasFlag(PatternType.Gathered)) + { + last++; + if (last == TotalColumns) + last = RandomStart; + } + else + last = GetRandomColumn(); + return last; + } } /// @@ -286,17 +288,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The containing the hit objects. private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) { + if (convertType.HasFlag(PatternType.ForceNotStack)) + return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); + var pattern = new Pattern(); bool addToCentre; int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre); int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2; - int nextColumn = Random.Next(RandomStart, columnLimit); + int nextColumn = GetRandomColumn(upperBound: columnLimit); for (int i = 0; i < noteCount; i++) { - while (pattern.ColumnHasObject(nextColumn)) - nextColumn = Random.Next(RandomStart, columnLimit); + nextColumn = FindAvailableColumn(nextColumn, upperBound: columnLimit, patterns: pattern); // Add normal note addToPattern(pattern, nextColumn); @@ -368,9 +372,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { addToCentre = false; - if (convertType.HasFlag(PatternType.ForceNotStack)) - return getRandomNoteCount(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); - switch (TotalColumns) { case 2: diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index 55081e5822..05ca1d4365 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Objects; @@ -90,6 +91,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } private double? conversionDifficulty; + /// /// A difficulty factor used for various conversion methods from osu!stable. /// @@ -116,5 +118,82 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return conversionDifficulty.Value; } } + + /// + /// Finds a new column in which a can be placed. + /// This uses to pick the next candidate column. + /// + /// The initial column to test. This may be returned if it is already a valid column. + /// A list of patterns for which the validity of a column should be checked against. + /// A column is not a valid candidate if a occupies the same column in any of the patterns. + /// A column for which there are no s in any of occupying the same column. + /// If there are no valid candidate columns. + protected int FindAvailableColumn(int initialColumn, params Pattern[] patterns) + => FindAvailableColumn(initialColumn, null, patterns: patterns); + + /// + /// Finds a new column in which a can be placed. + /// + /// The initial column to test. This may be returned if it is already a valid column. + /// A function to retrieve the next column. If null, a randomisation scheme will be used. + /// A function to perform additional validation checks to determine if a column is a valid candidate for a . + /// The minimum column index. If null, is used. + /// The maximum column index. If null, is used. + /// A list of patterns for which the validity of a column should be checked against. + /// A column is not a valid candidate if a occupies the same column in any of the patterns. + /// A column which has passed the check and for which there are no + /// s in any of occupying the same column. + /// If there are no valid candidate columns. + protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func nextColumn = null, [InstantHandle] Func validation = null, + params Pattern[] patterns) + { + lowerBound = lowerBound ?? RandomStart; + upperBound = upperBound ?? TotalColumns; + nextColumn = nextColumn ?? (_ => GetRandomColumn(lowerBound, upperBound)); + + // Check for the initial column + if (isValid(initialColumn)) + return initialColumn; + + // Ensure that we have at least one free column, so that an endless loop is avoided + bool hasValidColumns = false; + for (int i = lowerBound.Value; i < upperBound.Value; i++) + { + hasValidColumns = isValid(i); + if (hasValidColumns) + break; + } + + if (!hasValidColumns) + throw new NotEnoughColumnsException(); + + // Iterate until a valid column is found. This is a random iteration in the default case. + do + { + initialColumn = nextColumn(initialColumn); + } while (!isValid(initialColumn)); + + return initialColumn; + + bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column)); + } + + /// + /// Returns a random column index in the range [, ). + /// + /// The minimum column index. If null, is used. + /// The maximum column index. If null, is used. + protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns); + + /// + /// Occurs when mania conversion is stuck in an infinite loop unable to find columns to place new hitobjects in. + /// + public class NotEnoughColumnsException : Exception + { + public NotEnoughColumnsException() + : base("There were not enough columns to complete conversion.") + { + } + } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/HoldNoteMask.cs b/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/HoldNoteMask.cs new file mode 100644 index 0000000000..bfa6bc0a17 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/HoldNoteMask.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +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 OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays +{ + public class HoldNoteMask : HitObjectMask + { + public new DrawableHoldNote HitObject => (DrawableHoldNote)base.HitObject; + + private readonly IBindable direction = new Bindable(); + + private readonly BodyPiece body; + + public HoldNoteMask(DrawableHoldNote hold) + : base(hold) + { + InternalChildren = new Drawable[] + { + new HoldNoteNoteMask(hold.Head), + new HoldNoteNoteMask(hold.Tail), + body = new BodyPiece + { + AccentColour = Color4.Transparent + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, IScrollingInfo scrollingInfo) + { + body.BorderColour = colours.Yellow; + + direction.BindTo(scrollingInfo.Direction); + } + + protected override void Update() + { + base.Update(); + + Size = HitObject.DrawSize + new Vector2(0, HitObject.Tail.DrawHeight); + Position = Parent.ToLocalSpace(HitObject.ScreenSpaceDrawQuad.TopLeft); + + // This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do + // When scrolling upwards our origin is already at the top of the head note (which is the intended location), + // but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note) + if (direction.Value == ScrollingDirection.Down) + Y -= HitObject.Tail.DrawHeight; + } + + private class HoldNoteNoteMask : NoteMask + { + public HoldNoteNoteMask(DrawableNote note) + : base(note) + { + Select(); + } + + protected override void Update() + { + base.Update(); + + Anchor = HitObject.Anchor; + Origin = HitObject.Origin; + + Position = HitObject.DrawPosition; + } + + // Todo: This is temporary, since the note masks don't do anything special yet. In the future they will handle input. + public override bool HandleMouseInput => false; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/NoteMask.cs b/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/NoteMask.cs new file mode 100644 index 0000000000..78f876cb14 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/NoteMask.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; + +namespace osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays +{ + public class NoteMask : HitObjectMask + { + public NoteMask(DrawableNote note) + : base(note) + { + Scale = note.Scale; + + CornerRadius = 5; + Masking = true; + + AddInternal(new NotePiece()); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Yellow; + } + + protected override void Update() + { + base.Update(); + + Size = HitObject.DrawSize; + Position = Parent.ToLocalSpace(HitObject.ScreenSpaceDrawQuad.TopLeft); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditPlayfield.cs b/osu.Game.Rulesets.Mania/Edit/ManiaEditPlayfield.cs new file mode 100644 index 0000000000..e7bc526471 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaEditPlayfield.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.UI; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaEditPlayfield : ManiaPlayfield + { + public ManiaEditPlayfield(List stages) + : base(stages) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs new file mode 100644 index 0000000000..a01947a60b --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using OpenTK; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaEditRulesetContainer : ManiaRulesetContainer + { + public ManiaEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) + { + } + + protected override Playfield CreatePlayfield() => new ManiaEditPlayfield(Beatmap.Stages) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + protected override Vector2 PlayfieldArea => Vector2.One; + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs new file mode 100644 index 0000000000..f37d8134ce --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.Mania.UI; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaHitObjectComposer : HitObjectComposer + { + protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config; + + public ManiaHitObjectComposer(Ruleset ruleset) + : base(ruleset) + { + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(new ManiaScrollingInfo(Config)); + return dependencies; + } + + protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new ManiaEditRulesetContainer(ruleset, beatmap); + + protected override IReadOnlyList CompositionTools => new ICompositionTool[] + { + new HitObjectCompositionTool("Note"), + new HitObjectCompositionTool("Hold"), + }; + + public override HitObjectMask CreateMaskFor(DrawableHitObject hitObject) + { + switch (hitObject) + { + case DrawableNote note: + return new NoteMask(note); + case DrawableHoldNote holdNote: + return new HoldNoteMask(holdNote); + } + + return base.CreateMaskFor(hitObject); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs deleted file mode 100644 index 3a4beda77d..0000000000 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Mania.Judgements -{ - public class HoldNoteTailJudgement : ManiaJudgement - { - /// - /// Whether the hold note has been released too early and shouldn't give full score for the release. - /// - public bool HasBroken; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return base.NumericResultFor(result); - case HitResult.Great: - case HitResult.Perfect: - return base.NumericResultFor(HasBroken ? HitResult.Good : result); - } - } - } -} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 1cd1714705..19e89c8ec5 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -19,9 +19,11 @@ using osu.Game.Configuration; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; +using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania @@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.Mania public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap); public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score); + public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); + public override IEnumerable ConvertLegacyMods(LegacyMods mods) { if (mods.HasFlag(LegacyMods.Nightcore)) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 597450f223..6a0457efc6 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using OpenTK.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Judgements; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; @@ -19,10 +18,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableHoldNote : DrawableManiaHitObject, IKeyBindingHandler { - public override bool DisplayJudgement => false; + public override bool DisplayResult => false; - private readonly DrawableNote head; - private readonly DrawableNote tail; + public readonly DrawableNote Head; + public readonly DrawableNote Tail; private readonly BodyPiece bodyPiece; @@ -57,12 +56,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables HoldStartTime = () => holdStartTime }) }, - head = new DrawableHeadNote(this) + Head = new DrawableHeadNote(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, - tail = new DrawableTailNote(this) + Tail = new DrawableTailNote(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre @@ -72,8 +71,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables foreach (var tick in tickContainer) AddNested(tick); - AddNested(head); - AddNested(tail); + AddNested(Head); + AddNested(Tail); } protected override void OnDirectionChanged(ScrollingDirection direction) @@ -91,16 +90,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.AccentColour = value; bodyPiece.AccentColour = value; - head.AccentColour = value; - tail.AccentColour = value; + Head.AccentColour = value; + Tail.AccentColour = value; tickContainer.ForEach(t => t.AccentColour = value); } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (tail.AllJudged) - AddJudgement(new HoldNoteJudgement { Result = HitResult.Perfect }); + if (Tail.AllJudged) + ApplyResult(r => r.Type = HitResult.Perfect); } protected override void Update() @@ -108,8 +107,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.Update(); // Make the body piece not lie under the head note - bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * head.Height / 2; - bodyPiece.Height = DrawHeight - head.Height / 2 + tail.Height / 2; + bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; + bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; + } + + protected void BeginHold() + { + holdStartTime = Time.Current; + bodyPiece.Hitting = true; + } + + protected void EndHold() + { + holdStartTime = null; + bodyPiece.Hitting = false; } public bool OnPressed(ManiaAction action) @@ -124,8 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // The user has pressed during the body of the hold note, after the head note and its hit windows have passed // and within the limited range of the above if-statement. This state will be managed by the head note if the // user has pressed during the hit windows of the head note. - holdStartTime = Time.Current; - + BeginHold(); return true; } @@ -138,10 +148,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; - holdStartTime = null; + EndHold(); // If the key has been released too early, the user should not receive full score for the release - if (!tail.IsHit) + if (!Tail.IsHit) hasBroken = true; return true; @@ -166,12 +176,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return false; // If the key has been released too early, the user should not receive full score for the release - if (Judgements.Any(j => j.Result == HitResult.Miss)) + if (Result.Type == HitResult.Miss) holdNote.hasBroken = true; // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held // The body doesn't handle these early early hits, so we have to explicitly set the holding state here - holdNote.holdStartTime = Time.Current; + holdNote.BeginHold(); return true; } @@ -197,7 +207,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables this.holdNote = holdNote; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { // Factor in the release lenience timeOffset /= release_window_lenience; @@ -205,13 +215,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - { - AddJudgement(new HoldNoteTailJudgement - { - Result = HitResult.Miss, - HasBroken = holdNote.hasBroken - }); - } + ApplyResult(r => r.Type = HitResult.Miss); return; } @@ -220,10 +224,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (result == HitResult.None) return; - AddJudgement(new HoldNoteTailJudgement + ApplyResult(r => { - Result = result, - HasBroken = holdNote.hasBroken + if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect)) + result = HitResult.Good; + + r.Type = result; }); } @@ -238,7 +244,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; - UpdateJudgement(true); + UpdateResult(true); // Handled by the hold note, which will set holding = false return false; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index 5df6079efa..01d5bc6fd4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -7,7 +7,6 @@ using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Judgements; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Scoring; @@ -72,29 +71,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!userTriggered) - return; - if (Time.Current < HitObject.StartTime) return; - if (HoldStartTime?.Invoke() > HitObject.StartTime) - return; + var startTime = HoldStartTime?.Invoke(); - AddJudgement(new HoldNoteTickJudgement { Result = HitResult.Perfect }); - } - - protected override void Update() - { - if (AllJudged) - return; - - if (HoldStartTime?.Invoke() == null) - return; - - UpdateJudgement(true); + if (startTime == null || startTime > HitObject.StartTime) + ApplyResult(r => r.Type = HitResult.Miss); + else + ApplyResult(r => r.Type = HitResult.Perfect); } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 18084c4c08..7567f40b2f 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -6,7 +6,6 @@ using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; @@ -56,12 +55,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - AddJudgement(new ManiaJudgement { Result = HitResult.Miss }); + ApplyResult(r => r.Type = HitResult.Miss); return; } @@ -69,7 +68,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (result == HitResult.None) return; - AddJudgement(new ManiaJudgement { Result = result }); + ApplyResult(r => r.Type = result); } public virtual bool OnPressed(ManiaAction action) @@ -77,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; - return UpdateJudgement(true); + return UpdateResult(true); } public virtual bool OnReleased(ManiaAction action) => false; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs index 4ab2da208a..619fe06c73 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs @@ -32,6 +32,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces background = new Box { RelativeSizeAxes = Axes.Both }, foreground = new BufferedContainer { + Blending = BlendingMode.Additive, RelativeSizeAxes = Axes.Both, CacheDrawnFrameBuffer = true, Children = new Drawable[] @@ -73,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } private Color4 accentColour; + public Color4 AccentColour { get { return accentColour; } @@ -86,6 +88,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } } + public bool Hitting + { + get { return hitting; } + set + { + hitting = value; + updateAccentColour(); + } + } + private Cached subtractionCache = new Cached(); public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) @@ -118,13 +130,26 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } } + private bool hitting; + private void updateAccentColour() { if (!IsLoaded) return; - foreground.Colour = AccentColour.Opacity(0.9f); - background.Colour = AccentColour.Opacity(0.6f); + foreground.Colour = AccentColour.Opacity(0.5f); + background.Colour = AccentColour.Opacity(0.7f); + + const float animation_length = 50; + + foreground.ClearTransforms(false, nameof(foreground.Colour)); + if (hitting) + { + // wait for the next sync point + double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2); + using (foreground.BeginDelayedSequence(synchronisedOffset)) + foreground.FadeColour(AccentColour.Lighten(0.2f), animation_length).Then().FadeColour(foreground.Colour, animation_length).Loop(); + } subtractionCache.Invalidate(); } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 22fa93a308..77562bb4c2 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -3,6 +3,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.Types; namespace osu.Game.Rulesets.Mania.Objects @@ -55,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// The tail note of the hold. /// - public readonly Note Tail = new Note(); + public readonly TailNote Tail = new TailNote(); /// /// The time between ticks of this hold. @@ -68,9 +70,6 @@ namespace osu.Game.Rulesets.Mania.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate; - - Head.ApplyDefaults(controlPointInfo, difficulty); - Tail.ApplyDefaults(controlPointInfo, difficulty); } protected override void CreateNestedHitObjects() @@ -78,6 +77,9 @@ namespace osu.Game.Rulesets.Mania.Objects base.CreateNestedHitObjects(); createTicks(); + + AddNested(Head); + AddNested(Tail); } private void createTicks() @@ -94,5 +96,7 @@ namespace osu.Game.Rulesets.Mania.Objects }); } } + + public override Judgement CreateJudgement() => new HoldNoteJudgement(); } } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs index d078c15c92..05959a31c0 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs @@ -1,6 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; + namespace osu.Game.Rulesets.Mania.Objects { /// @@ -8,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Objects /// public class HoldNoteTick : ManiaHitObject { + public override Judgement CreateJudgement() => new HoldNoteTickJudgement(); } } diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 975188e550..42877649d2 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.cs @@ -1,6 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; + namespace osu.Game.Rulesets.Mania.Objects { /// @@ -8,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Objects /// public class Note : ManiaHitObject { + public override Judgement CreateJudgement() => new ManiaJudgement(); } } diff --git a/osu.Game.Rulesets.Mania/Objects/TailNote.cs b/osu.Game.Rulesets.Mania/Objects/TailNote.cs new file mode 100644 index 0000000000..9de542bcd3 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/TailNote.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; + +namespace osu.Game.Rulesets.Mania.Objects +{ + public class TailNote : Note + { + public override Judgement CreateJudgement() => new ManiaJudgement(); + } +} diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 4c955a680e..12b32c46ee 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; @@ -97,31 +96,20 @@ namespace osu.Game.Rulesets.Mania.Scoring { } - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void ApplyBeatmap(Beatmap beatmap) { + base.ApplyBeatmap(beatmap); + BeatmapDifficulty difficulty = beatmap.BeatmapInfo.BaseDifficulty; hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max); hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max); + } + protected override void SimulateAutoplay(Beatmap beatmap) + { while (true) { - foreach (var obj in beatmap.HitObjects) - { - var holdNote = obj as HoldNote; - - if (holdNote != null) - { - // Head - AddJudgement(new ManiaJudgement { Result = HitResult.Perfect }); - - // Ticks - int tickCount = holdNote.NestedHitObjects.OfType().Count(); - for (int i = 0; i < tickCount; i++) - AddJudgement(new HoldNoteTickJudgement { Result = HitResult.Perfect }); - } - - AddJudgement(new ManiaJudgement { Result = HitResult.Perfect }); - } + base.SimulateAutoplay(beatmap); if (!HasFailed) break; @@ -133,20 +121,20 @@ namespace osu.Game.Rulesets.Mania.Scoring } } - protected override void OnNewJudgement(Judgement judgement) + protected override void ApplyResult(JudgementResult result) { - base.OnNewJudgement(judgement); + base.ApplyResult(result); - bool isTick = judgement is HoldNoteTickJudgement; + bool isTick = result.Judgement is HoldNoteTickJudgement; if (isTick) { - if (judgement.IsHit) + if (result.IsHit) Health.Value += hpMultiplier * hp_increase_tick; } else { - switch (judgement.Result) + switch (result.Type) { case HitResult.Miss: Health.Value += hpMissMultiplier * hp_increase_miss; diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 877189dd61..d489d48fc3 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -131,14 +131,14 @@ namespace osu.Game.Rulesets.Mania.UI public override void Add(DrawableHitObject hitObject) { hitObject.AccentColour = AccentColour; - hitObject.OnJudgement += OnJudgement; + hitObject.OnNewResult += OnNewResult; HitObjects.Add(hitObject); } - internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { - if (!judgement.IsHit || !judgedObject.DisplayJudgement || !DisplayJudgements) + if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements) return; explosionContainer.Add(new HitExplosion(judgedObject) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 6566d44ef5..dc66249cd9 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -8,10 +8,10 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI { - internal class DrawableManiaJudgement : DrawableJudgement + public class DrawableManiaJudgement : DrawableJudgement { - public DrawableManiaJudgement(Judgement judgement, DrawableHitObject judgedObject) - : base(judgement, judgedObject) + public DrawableManiaJudgement(JudgementResult result, DrawableHitObject judgedObject) + : base(result, judgedObject) { } @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.UI this.FadeInFromZero(50, Easing.OutQuint); - if (Judgement.IsHit) + if (Result.IsHit) { this.ScaleTo(0.8f); this.ScaleTo(1, 250, Easing.OutElastic); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index f88169726e..999f84ed8e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -1,22 +1,20 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mania.Objects; using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI { public class ManiaPlayfield : ManiaScrollingPlayfield { - public List Columns => stages.SelectMany(x => x.Columns).ToList(); private readonly List stages = new List(); public ManiaPlayfield(List stageDefinitions) @@ -28,11 +26,11 @@ namespace osu.Game.Rulesets.Mania.UI throw new ArgumentException("Can't have zero or fewer stages."); GridContainer playfieldGrid; - InternalChild = playfieldGrid = new GridContainer + AddInternal(playfieldGrid = new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] { new Drawable[stageDefinitions.Count] } - }; + }); var normalColumnAction = ManiaAction.Key1; var specialColumnAction = ManiaAction.Special1; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index aaa4505b5e..09ebde2799 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Input; @@ -35,8 +34,7 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - private readonly Bindable configDirection = new Bindable(); - private ScrollingInfo scrollingInfo; + protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config; public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -73,9 +71,6 @@ namespace osu.Game.Rulesets.Mania.UI private void load() { BarLines.ForEach(Playfield.Add); - - ((ManiaConfigManager)Config).BindWith(ManiaSetting.ScrollDirection, configDirection); - configDirection.BindValueChanged(d => scrollingInfo.Direction.Value = (ScrollingDirection)d, true); } private DependencyContainer dependencies; @@ -83,11 +78,14 @@ namespace osu.Game.Rulesets.Mania.UI protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(scrollingInfo = new ScrollingInfo()); + + if (dependencies.Get() == null) + dependencies.CacheAs(new ManiaScrollingInfo(Config)); + return dependencies; } - protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages) + protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -115,11 +113,5 @@ namespace osu.Game.Rulesets.Mania.UI protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); - - private class ScrollingInfo : IScrollingInfo - { - public readonly Bindable Direction = new Bindable(); - IBindable IScrollingInfo.Direction => Direction; - } } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs new file mode 100644 index 0000000000..624ea13e1b --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class ManiaScrollingInfo : IScrollingInfo + { + private readonly Bindable configDirection = new Bindable(); + + public readonly Bindable Direction = new Bindable(); + IBindable IScrollingInfo.Direction => Direction; + + public ManiaScrollingInfo(ManiaConfigManager config) + { + config.BindWith(ManiaSetting.ScrollDirection, configDirection); + configDirection.BindValueChanged(v => Direction.Value = (ScrollingDirection)v, true); + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index f386cf15a2..f292d5ff16 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// A collection of s. /// - internal class ManiaStage : ManiaScrollingPlayfield + public class ManiaStage : ManiaScrollingPlayfield { public const float HIT_TARGET_POSITION = 50; @@ -156,18 +156,18 @@ namespace osu.Game.Rulesets.Mania.UI var maniaObject = (ManiaHitObject)h.HitObject; int columnIndex = maniaObject.Column - firstColumnIndex; Columns.ElementAt(columnIndex).Add(h); - h.OnJudgement += OnJudgement; + h.OnNewResult += OnNewResult; } public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline)); - internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { - if (!judgedObject.DisplayJudgement || !DisplayJudgements) + if (!judgedObject.DisplayResult || !DisplayJudgements) return; judgements.Clear(); - judgements.Add(new DrawableManiaJudgement(judgement, judgedObject) + judgements.Add(new DrawableManiaJudgement(result, judgedObject) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs new file mode 100644 index 0000000000..579cb77084 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.IO; +using System.Linq; +using System.Text; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class StackingTest + { + [Test] + public void TestStacking() + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(beatmap_data))) + using (var reader = new StreamReader(stream)) + { + var beatmap = Decoder.GetDecoder(reader).Decode(reader); + var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo); + + var objects = converted.HitObjects.ToList(); + + // The last hitobject triggers the stacking + for (int i = 0; i < objects.Count - 1; i++) + Assert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight); + } + } + + private const string beatmap_data = @" +osu file format v14 + +[General] +StackLeniency: 0.2 + +[Difficulty] +ApproachRate:9.2 +SliderMultiplier:1 +SliderTickRate:0.5 + +[TimingPoints] +217871,6400,4,2,1,20,1,0 +217871,-800,4,2,1,20,0,0 +218071,-787.5,4,2,1,20,0,0 +218271,-775,4,2,1,20,0,0 +218471,-762.5,4,2,1,20,0,0 +218671,-750,4,2,1,20,0,0 +240271,-10,4,2,0,5,0,0 + +[HitObjects] +311,185,217871,6,0,L|318:158,1,25 +311,185,218071,2,0,L|335:170,1,25 +311,185,218271,2,0,L|338:192,1,25 +311,185,218471,2,0,L|325:209,1,25 +311,185,218671,2,0,L|304:212,1,25 +311,185,240271,5,0,0:0:0:0: +"; + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs index 7af7140fd8..6b67188791 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs @@ -5,12 +5,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; using OpenTK; -using osu.Game.Rulesets.Osu.Judgements; using System.Collections.Generic; using System; using osu.Game.Rulesets.Mods; @@ -87,7 +85,7 @@ namespace osu.Game.Rulesets.Osu.Tests } } - private class TestDrawableHitCircle : DrawableHitCircle + protected class TestDrawableHitCircle : DrawableHitCircle { private readonly bool auto; @@ -96,19 +94,17 @@ namespace osu.Game.Rulesets.Osu.Tests this.auto = auto; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + public void TriggerJudgement() => UpdateResult(true); + + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (auto && !userTriggered && timeOffset > 0) { // force success - AddJudgement(new OsuJudgement - { - Result = HitResult.Great - }); - State.Value = ArmedState.Hit; + ApplyResult(r => r.Type = HitResult.Great); } else - base.CheckForJudgements(userTriggered, timeOffset); + base.CheckForResult(userTriggered, timeOffset); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseShaking.cs new file mode 100644 index 0000000000..97978cff1e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseShaking.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.MathUtils; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestCaseShaking : TestCaseHitCircle + { + public override void Add(Drawable drawable) + { + base.Add(drawable); + + if (drawable is TestDrawableHitCircle hitObject) + { + Scheduler.AddDelayed(() => hitObject.TriggerJudgement(), + hitObject.HitObject.StartTime - (hitObject.HitObject.HitWindows.HalfWindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs index cb1ea5cc5f..3f9464a98f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs @@ -304,13 +304,13 @@ namespace osu.Game.Rulesets.Osu.Tests foreach (var mod in Mods.OfType()) mod.ApplyToDrawableHitObjects(new[] { drawable }); - drawable.OnJudgement += onJudgement; + drawable.OnNewResult += onNewResult; Add(drawable); } private float judgementOffsetDirection = 1; - private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { var osuObject = judgedObject as DrawableOsuHitObject; if (osuObject == null) @@ -321,8 +321,8 @@ namespace osu.Game.Rulesets.Osu.Tests { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = judgement.IsHit ? "Hit!" : "Miss!", - Colour = judgement.IsHit ? Color4.Green : Color4.Red, + Text = result.IsHit ? "Hit!" : "Miss!", + Colour = result.IsHit ? Color4.Green : Color4.Red, TextSize = 30, Position = osuObject.HitObject.StackedEndPosition + judgementOffsetDirection * new Vector2(0, 45) }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSpinner.cs index b05a763e88..3b91ea93b8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSpinner.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Tests this.auto = auto; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1) { @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Tests auto = false; } - base.CheckForJudgements(userTriggered, timeOffset); + base.CheckForResult(userTriggered, timeOffset); } } } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 405493cde4..9e0e649eb2 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.UI; namespace osu.Game.Rulesets.Osu.Beatmaps { - internal class OsuBeatmapConverter : BeatmapConverter + public class OsuBeatmapConverter : BeatmapConverter { public OsuBeatmapConverter(IBeatmap beatmap) : base(beatmap) @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps RepeatCount = curveData.RepeatCount, Position = positionData?.Position ?? Vector2.Zero, NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset }; } @@ -52,7 +53,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps StartTime = original.StartTime, Samples = original.Samples, EndTime = endTimeData.EndTime, - Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2 + Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2, + NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, }; } else @@ -62,7 +65,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps StartTime = original.StartTime, Samples = original.Samples, Position = positionData?.Position ?? Vector2.Zero, - NewCombo = comboData?.NewCombo ?? false + NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, }; } } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index bbe2d67baa..cfb1b0f050 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -8,16 +8,16 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Beatmaps { - internal class OsuBeatmapProcessor : BeatmapProcessor + public class OsuBeatmapProcessor : BeatmapProcessor { public OsuBeatmapProcessor(IBeatmap beatmap) : base(beatmap) { } - public override void PreProcess() + public override void PostProcess() { - base.PreProcess(); + base.PostProcess(); applyStacking((Beatmap)Beatmap); } diff --git a/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs b/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs new file mode 100644 index 0000000000..3000031c78 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public enum ComboResult + { + [Description(@"")] + None, + [Description(@"Good")] + Good, + [Description(@"Amazing")] + Perfect + } +} diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index 26becfdec9..b1c9760866 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Judgements @@ -25,7 +24,5 @@ namespace osu.Game.Rulesets.Osu.Judgements return 300; } } - - public ComboResult Combo; } } diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs new file mode 100644 index 0000000000..17b8b4399f --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Judgements; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuJudgementResult : JudgementResult + { + public ComboResult ComboType; + + public OsuJudgementResult(Judgement judgement) + : base(judgement) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 9f9c2a09b6..f3b7d60cf0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -2,14 +2,89 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Linq; +using osu.Framework.Input.States; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; +using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModRelax : ModRelax + public class OsuModRelax : ModRelax, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToRulesetContainer { public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); + + public bool AllowFail => false; + + public void Update(Playfield playfield) + { + bool requiresHold = false; + bool requiresHit = false; + + const float relax_leniency = 3; + + foreach (var drawable in playfield.HitObjectContainer.AliveObjects) + { + if (!(drawable is DrawableOsuHitObject osuHit)) + continue; + + double time = osuHit.Clock.CurrentTime; + double relativetime = time - osuHit.HitObject.StartTime; + + if (time < osuHit.HitObject.StartTime - relax_leniency) continue; + + if (osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime || osuHit.IsHit) + continue; + + requiresHit |= osuHit is DrawableHitCircle && osuHit.IsHovered && osuHit.HitObject.HitWindows.CanBeHit(relativetime); + requiresHold |= osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered) || osuHit is DrawableSpinner; + } + + if (requiresHit) + { + addAction(false); + addAction(true); + } + + addAction(requiresHold); + } + + private bool wasHit; + private bool wasLeft; + + private OsuInputManager osuInputManager; + + private void addAction(bool hitting) + { + if (wasHit == hitting) + return; + + wasHit = hitting; + + var state = new ReplayState + { + PressedActions = new List() + }; + + if (hitting) + { + state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton); + wasLeft = !wasLeft; + } + + osuInputManager.HandleCustomInput(new InputState(), state); + } + + public void ApplyToRulesetContainer(RulesetContainer rulesetContainer) + { + // grab the input manager for future use. + osuInputManager = (OsuInputManager)rulesetContainer.KeyBindingInputManager; + osuInputManager.AllowUserPresses = false; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index c525b4bd97..4bdddcef11 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using OpenTK; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using OpenTK.Graphics; @@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (AllJudged) return false; - UpdateJudgement(true); + UpdateResult(true); return true; }, }, @@ -77,23 +76,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - AddJudgement(new OsuJudgement { Result = HitResult.Miss }); + ApplyResult(r => r.Type = HitResult.Miss); + return; } var result = HitObject.HitWindows.ResultFor(timeOffset); if (result == HitResult.None) - return; - - AddJudgement(new OsuJudgement { - Result = result, - }); + Shake(Math.Abs(timeOffset) - HitObject.HitWindows.HalfWindowFor(HitResult.Miss)); + return; + } + + ApplyResult(r => r.Type = result); } protected override void UpdatePreemptState() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 5dc141bed0..10cd246172 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -2,27 +2,37 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.ComponentModel; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; -using System.Linq; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using OpenTK.Graphics; +using osu.Game.Graphics.Containers; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuHitObject : DrawableHitObject { - public override bool IsPresent => base.IsPresent || State.Value == ArmedState.Idle && Time.Current >= HitObject.StartTime - HitObject.TimePreempt; + public override bool IsPresent => base.IsPresent || State.Value == ArmedState.Idle && Clock?.CurrentTime >= HitObject.StartTime - HitObject.TimePreempt; + + private readonly ShakeContainer shakeContainer; protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) { + base.AddInternal(shakeContainer = new ShakeContainer { RelativeSizeAxes = Axes.Both }); Alpha = 0; } + // Forward all internal management to shakeContainer. + // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690) + protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable); + protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren); + protected override bool RemoveInternal(Drawable drawable) => shakeContainer.Remove(drawable); + protected sealed override void UpdateState(ArmedState state) { double transformTime = HitObject.StartTime - HitObject.TimePreempt; @@ -34,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { UpdatePreemptState(); - var judgementOffset = Math.Min(HitObject.HitWindows.HalfWindowFor(HitResult.Miss), Judgements.FirstOrDefault()?.TimeOffset ?? 0); + var judgementOffset = Math.Min(HitObject.HitWindows.HalfWindowFor(HitResult.Miss), Result?.TimeOffset ?? 0); using (BeginDelayedSequence(HitObject.TimePreempt + judgementOffset, true)) UpdateCurrentState(state); @@ -57,20 +67,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Todo: At some point we need to move these to DrawableHitObject after ensuring that all other Rulesets apply // transforms in the same way and don't rely on them not being cleared - public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) { } - public override void ApplyTransformsAt(double time, bool propagateChildren = false) { } + public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) + { + } + + public override void ApplyTransformsAt(double time, bool propagateChildren = false) + { + } private OsuInputManager osuActionInputManager; internal OsuInputManager OsuActionInputManager => osuActionInputManager ?? (osuActionInputManager = GetContainingInputManager() as OsuInputManager); - } - public enum ComboResult - { - [Description(@"")] - None, - [Description(@"Good")] - Good, - [Description(@"Amazing")] - Perfect + protected virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); + + protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(judgement); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index e8743281da..04ec3f13c7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -11,14 +11,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuJudgement : DrawableJudgement { - public DrawableOsuJudgement(Judgement judgement, DrawableHitObject judgedObject) - : base(judgement, judgedObject) + public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject) + : base(result, judgedObject) { } protected override void LoadComplete() { - if (Judgement.Result != HitResult.Miss) + if (Result.Type != HitResult.Miss) JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); base.LoadComplete(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index 6bff1380d6..dfe7937e81 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -8,7 +8,6 @@ using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Drawables; using OpenTK; using osu.Game.Graphics; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; @@ -42,10 +41,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (repeatPoint.StartTime <= Time.Current) - AddJudgement(new OsuJudgement { Result = drawableSlider.Tracking ? HitResult.Great : HitResult.Miss }); + ApplyResult(r => r.Type = drawableSlider.Tracking ? HitResult.Great : HitResult.Miss); } protected override void UpdatePreemptState() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index f1907a92a8..66f491532d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; using OpenTK.Graphics; @@ -45,14 +44,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }, ticks = new Container { RelativeSizeAxes = Axes.Both }, repeatPoints = new Container { RelativeSizeAxes = Axes.Both }, - Ball = new SliderBall(s) + Ball = new SliderBall(s, this) { BypassAutoSizeAxes = Axes.Both, Scale = new Vector2(s.Scale), AlwaysPresent = true, Alpha = 0 }, - HeadCircle = new DrawableSliderHead(s, s.HeadCircle), + HeadCircle = new DrawableSliderHead(s, s.HeadCircle) + { + OnShake = Shake + }, TailCircle = new DrawableSliderTail(s, s.TailCircle) }; @@ -132,23 +134,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!userTriggered && Time.Current >= slider.EndTime) + if (userTriggered || Time.Current < slider.EndTime) + return; + + ApplyResult(r => { var judgementsCount = NestedHitObjects.Count(); var judgementsHit = NestedHitObjects.Count(h => h.IsHit); var hitFraction = (double)judgementsHit / judgementsCount; - if (hitFraction == 1 && HeadCircle.Judgements.Any(j => j.Result == HitResult.Great)) - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - else if (hitFraction >= 0.5 && HeadCircle.Judgements.Any(j => j.Result >= HitResult.Good)) - AddJudgement(new OsuJudgement { Result = HitResult.Good }); + + if (hitFraction == 1 && HeadCircle.Result.Type == HitResult.Great) + r.Type = HitResult.Great; + else if (hitFraction >= 0.5 && HeadCircle.Result.Type >= HitResult.Good) + r.Type = HitResult.Good; else if (hitFraction > 0) - AddJudgement(new OsuJudgement { Result = HitResult.Meh }); + r.Type = HitResult.Meh; else - AddJudgement(new OsuJudgement { Result = HitResult.Miss }); - } + r.Type = HitResult.Miss; + }); } protected override void UpdateCurrentState(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index e823c870f9..6d6cba4936 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using osu.Game.Rulesets.Objects.Types; using OpenTK; @@ -28,5 +29,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!IsHit) Position = slider.CurvePositionAt(completionProgress); } + + public Action OnShake; + + protected override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index fee663963e..45c925b87a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -12,11 +11,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// The judgement text is provided by the . /// - public override bool DisplayJudgement => false; + public override bool DisplayResult => false; public bool Tracking { get; set; } - public DrawableSliderTail(Slider slider, HitCircle hitCircle) + public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle) : base(hitCircle) { Origin = Anchor.Centre; @@ -29,10 +28,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Position = HitObject.Position - slider.Position; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered && timeOffset >= 0) - AddJudgement(new OsuSliderTailJudgement { Result = Tracking ? HitResult.Great : HitResult.Miss }); + ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index a5ecb63d12..964c75131a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -6,7 +6,6 @@ using osu.Game.Rulesets.Objects.Drawables; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Framework.Graphics.Containers; @@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } - public override bool DisplayJudgement => false; + public override bool DisplayResult => false; public DrawableSliderTick(SliderTick sliderTick) : base(sliderTick) { @@ -48,10 +47,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset >= 0) - AddJudgement(new OsuJudgement { Result = Tracking ? HitResult.Great : HitResult.Miss }); + ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); } protected override void UpdatePreemptState() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 1d3df69fb8..51b1990a21 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -11,7 +11,6 @@ using OpenTK.Graphics; using osu.Game.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Screens.Ranking; using osu.Game.Rulesets.Scoring; @@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public float Progress => MathHelper.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1); - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Time.Current < HitObject.StartTime) return; @@ -136,17 +135,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables glow.FadeColour(completeColour, duration); } - if (!userTriggered && Time.Current >= Spinner.EndTime) + if (userTriggered || Time.Current < Spinner.EndTime) + return; + + ApplyResult(r => { if (Progress >= 1) - AddJudgement(new OsuJudgement { Result = HitResult.Great }); + r.Type = HitResult.Great; else if (Progress > .9) - AddJudgement(new OsuJudgement { Result = HitResult.Good }); + r.Type = HitResult.Good; else if (Progress > .75) - AddJudgement(new OsuJudgement { Result = HitResult.Meh }); + r.Type = HitResult.Meh; else if (Time.Current >= Spinner.EndTime) - AddJudgement(new OsuJudgement { Result = HitResult.Miss }); - } + r.Type = HitResult.Miss; + }); } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 182cf66df8..b79750a1b3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.EventArgs; using osu.Framework.Input.States; using osu.Game.Rulesets.Objects.Types; -using OpenTK; using OpenTK.Graphics; using osu.Game.Skinning; @@ -37,9 +36,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private readonly Slider slider; public readonly Drawable FollowCircle; private Drawable drawableBall; + private readonly DrawableSlider drawableSlider; - public SliderBall(Slider slider) + public SliderBall(Slider slider, DrawableSlider drawableSlider = null) { + this.drawableSlider = drawableSlider; this.slider = slider; Masking = true; AutoSizeAxes = Axes.Both; @@ -121,9 +122,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return base.OnMouseMove(state); } - // If the current time is between the start and end of the slider, we should track mouse input regardless of the cursor position. - public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => canCurrentlyTrack || base.ReceiveMouseInputAt(screenSpacePos); - public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) { // Consider the case of rewinding - children's transforms are handled internally, so propagating down @@ -158,8 +156,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces // Make sure to use the base version of ReceiveMouseInputAt so that we correctly check the position. Tracking = canCurrentlyTrack && lastState != null - && base.ReceiveMouseInputAt(lastState.Mouse.NativeState.Position) - && ((Parent as DrawableSlider)?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); + && ReceiveMouseInputAt(lastState.Mouse.NativeState.Position) + && (drawableSlider?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 283d6b91f6..6f0197e711 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -7,14 +7,15 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Textures; -using OpenTK; using OpenTK.Graphics.ES30; using OpenTK.Graphics; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Objects.Types; +using OpenTK; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -43,6 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public double? SnakedEnd { get; private set; } private Color4 accentColour = Color4.White; + /// /// Used to colour the path. /// @@ -61,6 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } private Color4 borderColour = Color4.White; + /// /// Used to colour the path border. /// @@ -85,6 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private Vector2 topLeftOffset; private readonly Slider slider; + public SliderBody(Slider s) { slider = s; @@ -139,8 +143,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var texture = new Texture(textureWidth, 1); //initialise background - var raw = new RawTexture(textureWidth, 1); - var bytes = raw.Data; + var raw = new Image(textureWidth, 1); const float aa_portion = 0.02f; const float border_portion = 0.128f; @@ -155,19 +158,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (progress <= border_portion) { - bytes[i * 4] = (byte)(BorderColour.R * 255); - bytes[i * 4 + 1] = (byte)(BorderColour.G * 255); - bytes[i * 4 + 2] = (byte)(BorderColour.B * 255); - bytes[i * 4 + 3] = (byte)(Math.Min(progress / aa_portion, 1) * (BorderColour.A * 255)); + raw[i, 0] = new Rgba32(BorderColour.R, BorderColour.G, BorderColour.B, Math.Min(progress / aa_portion, 1) * BorderColour.A); } else { progress -= border_portion; - - bytes[i * 4] = (byte)(AccentColour.R * 255); - bytes[i * 4 + 1] = (byte)(AccentColour.G * 255); - bytes[i * 4 + 2] = (byte)(AccentColour.B * 255); - bytes[i * 4 + 3] = (byte)((opacity_at_edge - (opacity_at_edge - opacity_at_centre) * progress / gradient_portion) * (AccentColour.A * 255)); + raw[i, 0] = new Rgba32(AccentColour.R, AccentColour.G, AccentColour.B, + (opacity_at_edge - (opacity_at_edge - opacity_at_centre) * progress / gradient_portion) * AccentColour.A); } } diff --git a/osu.Game.Rulesets.Osu/Objects/HitCircle.cs b/osu.Game.Rulesets.Osu/Objects/HitCircle.cs index 9e309a376d..d1656a9672 100644 --- a/osu.Game.Rulesets.Osu/Objects/HitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/HitCircle.cs @@ -1,9 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; + namespace osu.Game.Rulesets.Osu.Objects { public class HitCircle : OsuHitObject { + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 48a6365c00..fdf5aaffa8 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -54,6 +54,8 @@ namespace osu.Game.Rulesets.Osu.Objects public virtual bool NewCombo { get; set; } + public int ComboOffset { get; set; } + public virtual int IndexInCurrentCombo { get; set; } public virtual int ComboIndex { get; set; } diff --git a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs index 3495bc1b4b..c8621cdbcf 100644 --- a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs @@ -4,6 +4,8 @@ using System; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.Objects { @@ -24,5 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects if (RepeatIndex > 0) TimePreempt = Math.Min(SpanDuration * 2, TimePreempt); } + + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 698f9de787..7a0dcc77a6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -10,6 +10,8 @@ using System.Linq; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.Objects { @@ -94,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Objects public double TickDistance; public HitCircle HeadCircle; - public HitCircle TailCircle; + public SliderTailCircle TailCircle; protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { @@ -133,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects ComboIndex = ComboIndex, }; - TailCircle = new SliderCircle(this) + TailCircle = new SliderTailCircle(this) { StartTime = EndTime, Position = EndPosition, @@ -211,5 +213,7 @@ namespace osu.Game.Rulesets.Osu.Objects }); } } + + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs new file mode 100644 index 0000000000..23616ea005 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SliderTailCircle : SliderCircle + { + public SliderTailCircle(Slider slider) + : base(slider) + { + } + + public override Judgement CreateJudgement() => new OsuSliderTailJudgement(); + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 54337a12be..906f0a0182 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -3,6 +3,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.Objects { @@ -26,5 +28,7 @@ namespace osu.Game.Rulesets.Osu.Objects TimePreempt = (StartTime - SpanStartTime) / 2 + offset; } + + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 503ad85674..1c60fd4831 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -5,6 +5,8 @@ using System; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.Objects { @@ -18,8 +20,6 @@ namespace osu.Game.Rulesets.Osu.Objects /// public int SpinsRequired { get; protected set; } = 1; - public override bool NewCombo => true; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -29,5 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); } + + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index d9ae836e0a..e7bbe755a0 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.ComponentModel; using osu.Framework.Input.Bindings; +using osu.Framework.Input.EventArgs; +using osu.Framework.Input.States; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu @@ -12,8 +14,35 @@ namespace osu.Game.Rulesets.Osu { public IEnumerable PressedActions => KeyBindingContainer.PressedActions; - public OsuInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique) + public bool AllowUserPresses { + set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value; + } + + protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + => new OsuKeyBindingContainer(ruleset, variant, unique); + + public OsuInputManager(RulesetInfo ruleset) + : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + + private class OsuKeyBindingContainer : RulesetKeyBindingContainer + { + public bool AllowUserPresses = true; + + public OsuKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => AllowUserPresses && base.OnKeyDown(state, args); + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => AllowUserPresses && base.OnKeyUp(state, args); + protected override bool OnJoystickPress(InputState state, JoystickEventArgs args) => AllowUserPresses && base.OnJoystickPress(state, args); + protected override bool OnJoystickRelease(InputState state, JoystickEventArgs args) => AllowUserPresses && base.OnJoystickRelease(state, args); + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => AllowUserPresses && base.OnMouseDown(state, args); + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => AllowUserPresses && base.OnMouseUp(state, args); + protected override bool OnScroll(InputState state) => AllowUserPresses && base.OnScroll(state); } } @@ -21,6 +50,7 @@ namespace osu.Game.Rulesets.Osu { [Description("Left Button")] LeftButton, + [Description("Right Button")] RightButton } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 01b92255ae..a9d39e88b4 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -2,13 +2,11 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using System.Linq; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -26,28 +24,11 @@ namespace osu.Game.Rulesets.Osu.Scoring private readonly Dictionary scoreResultCounts = new Dictionary(); private readonly Dictionary comboResultCounts = new Dictionary(); - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void ApplyBeatmap(Beatmap beatmap) { + base.ApplyBeatmap(beatmap); + hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; - - foreach (var obj in beatmap.HitObjects) - { - if (obj is Slider slider) - { - // Head - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - - // Ticks - foreach (var unused in slider.NestedHitObjects.OfType()) - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - - //Repeats - foreach (var unused in slider.NestedHitObjects.OfType()) - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - } - - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - } } protected override void Reset(bool storeResults) @@ -70,19 +51,19 @@ namespace osu.Game.Rulesets.Osu.Scoring private const double harshness = 0.01; - protected override void OnNewJudgement(Judgement judgement) + protected override void ApplyResult(JudgementResult result) { - base.OnNewJudgement(judgement); + base.ApplyResult(result); - var osuJudgement = (OsuJudgement)judgement; + var osuResult = (OsuJudgementResult)result; - if (judgement.Result != HitResult.None) + if (result.Type != HitResult.None) { - scoreResultCounts[judgement.Result] = scoreResultCounts.GetOrDefault(judgement.Result) + 1; - comboResultCounts[osuJudgement.Combo] = comboResultCounts.GetOrDefault(osuJudgement.Combo) + 1; + scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; + comboResultCounts[osuResult.ComboType] = comboResultCounts.GetOrDefault(osuResult.ComboType) + 1; } - switch (judgement.Result) + switch (result.Type) { case HitResult.Great: Health.Value += (10.2 - hpDrainRate) * harshness; @@ -105,5 +86,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break; } } + + protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(judgement); } } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 0532fe0223..4a6b12d41a 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor if (Shared.VertexBuffer == null) Shared.VertexBuffer = new QuadVertexBuffer(max_sprites, BufferUsageHint.DynamicDraw); - Shader.GetUniform("g_FadeClock").Value = Time; + Shader.GetUniform("g_FadeClock").UpdateValue(ref Time); int updateStart = -1, updateEnd = 0; for (int i = 0; i < Parts.Length; ++i) @@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Texture.DrawQuad( new Quad(pos.X - Size.X / 2, pos.Y - Size.Y / 2, Size.X, Size.Y), - DrawInfo.Colour, + DrawColourInfo.Colour, null, v => Shared.VertexBuffer.Vertices[end++] = new TexturedTrailVertex { diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index b0ba9afee6..61937a535c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI public override void Add(DrawableHitObject h) { - h.OnJudgement += onJudgement; + h.OnNewResult += onNewResult; var c = h as IDrawableHitObjectWithProxiedApproach; if (c != null) @@ -61,15 +61,15 @@ namespace osu.Game.Rulesets.Osu.UI public override void PostProcess() { - connectionLayer.HitObjects = HitObjects.Objects.Select(d => d.HitObject).OfType(); + connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType(); } - private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { - if (!judgedObject.DisplayJudgement || !DisplayJudgements) + if (!judgedObject.DisplayResult || !DisplayJudgements) return; - DrawableOsuJudgement explosion = new DrawableOsuJudgement(judgement, judgedObject) + DrawableOsuJudgement explosion = new DrawableOsuJudgement(result, judgedObject) { Origin = Anchor.Centre, Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition diff --git a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs index 1bf24a46bc..fc103e4c72 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs @@ -8,9 +8,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.MathUtils; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; @@ -39,8 +39,10 @@ namespace osu.Game.Rulesets.Taiko.Tests [BackgroundDependencyLoader] private void load() { - AddStep("Hit!", () => addHitJudgement(false)); + AddStep("Hit", () => addHitJudgement(false)); + AddStep("Strong hit", () => addStrongHitJudgement(false)); AddStep("Kiai hit", () => addHitJudgement(true)); + AddStep("Strong kiai hit", () => addStrongHitJudgement(true)); AddStep("Miss :(", addMissJudgement); AddStep("DrumRoll", () => addDrumRoll(false)); AddStep("Strong DrumRoll", () => addDrumRoll(true)); @@ -78,15 +80,12 @@ namespace osu.Game.Rulesets.Taiko.Tests ControlPointInfo = controlPointInfo }); - var rateAdjustClock = new StopwatchClock(true) { Rate = 1 }; - Add(playfieldContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Height = 768, - Clock = new FramedClock(rateAdjustClock), Children = new[] { rulesetContainer = new TaikoRulesetContainer(new TaikoRuleset(), beatmap) } }); } @@ -133,28 +132,35 @@ namespace osu.Game.Rulesets.Taiko.Tests HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; var cpi = new ControlPointInfo(); - cpi.EffectPoints.Add(new EffectControlPoint - { - KiaiMode = kiai - }); + cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai }); Hit hit = new Hit(); hit.ApplyDefaults(cpi, new BeatmapDifficulty()); var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; - ((TaikoPlayfield)rulesetContainer.Playfield).OnJudgement(h, new TaikoJudgement { Result = hitResult }); + ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(h, new JudgementResult(new TaikoJudgement()) { Type = hitResult }); + } - if (RNG.Next(10) == 0) - { - ((TaikoPlayfield)rulesetContainer.Playfield).OnJudgement(h, new TaikoJudgement { Result = hitResult }); - ((TaikoPlayfield)rulesetContainer.Playfield).OnJudgement(h, new TaikoStrongHitJudgement()); - } + private void addStrongHitJudgement(bool kiai) + { + HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; + + var cpi = new ControlPointInfo(); + cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai }); + + Hit hit = new Hit(); + hit.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; + + ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(h, new JudgementResult(new TaikoJudgement()) { Type = hitResult }); + ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new TaikoStrongJudgement()) { Type = HitResult.Great }); } private void addMissJudgement() { - ((TaikoPlayfield)rulesetContainer.Playfield).OnJudgement(new DrawableTestHit(new Hit()), new TaikoJudgement { Result = HitResult.Miss }); + ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(new DrawableTestHit(new Hit()), new JudgementResult(new TaikoJudgement()) { Type = HitResult.Miss }); } private void addBarLine(bool major, double delay = scroll_time) @@ -204,10 +210,7 @@ namespace osu.Game.Rulesets.Taiko.Tests h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - if (strong) - rulesetContainer.Playfield.Add(new DrawableCentreHitStrong(h)); - else - rulesetContainer.Playfield.Add(new DrawableCentreHit(h)); + rulesetContainer.Playfield.Add(new DrawableCentreHit(h)); } private void addRimHit(bool strong) @@ -220,10 +223,17 @@ namespace osu.Game.Rulesets.Taiko.Tests h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - if (strong) - rulesetContainer.Playfield.Add(new DrawableRimHitStrong(h)); - else - rulesetContainer.Playfield.Add(new DrawableRimHit(h)); + rulesetContainer.Playfield.Add(new DrawableRimHit(h)); + } + + private class TestStrongNestedHit : DrawableStrongNestedHit + { + public TestStrongNestedHit(DrawableHitObject mainObject) + : base(null, mainObject) + { + } + + public override bool OnPressed(TaikoAction action) => false; } private class DrawableTestHit : DrawableHitObject diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 41972b5d20..c2cde332e8 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => { TaikoHitObject first = x.First(); - if (x.Skip(1).Any()) + if (x.Skip(1).Any() && !(first is Swell)) first.IsStrong = true; return first; }).ToList(); @@ -168,7 +168,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - IsStrong = strong, Duration = endTimeData.Duration, RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier) }; diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs index 608f1f9be2..81a1bd1344 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs @@ -7,15 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoIntermediateSwellJudgement : TaikoJudgement { - public override HitResult MaxResult => HitResult.Perfect; + public override HitResult MaxResult => HitResult.Great; public override bool AffectsCombo => false; - public TaikoIntermediateSwellJudgement() - { - Final = false; - } - /// /// Computes the numeric result value for the combo portion of the score. /// diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongHitJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs similarity index 64% rename from osu.Game.Rulesets.Taiko/Judgements/TaikoStrongHitJudgement.cs rename to osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs index 288ad236aa..ccfdeb5b0e 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongHitJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs @@ -3,13 +3,8 @@ namespace osu.Game.Rulesets.Taiko.Judgements { - public class TaikoStrongHitJudgement : TaikoJudgement + public class TaikoStrongJudgement : TaikoJudgement { public override bool AffectsCombo => false; - - public TaikoStrongHitJudgement() - { - Final = true; - } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index dda96c2caf..a6e9972dd3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableCentreHit : DrawableHit { - protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; + public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; public DrawableCentreHit(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs deleted file mode 100644 index a2dabf2b18..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Game.Graphics; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public class DrawableCentreHitStrong : DrawableHitStrong - { - protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; - - public DrawableCentreHitStrong(Hit hit) - : base(hit) - { - MainPiece.Add(new CentreHitSymbolPiece()); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - MainPiece.AccentColour = colours.PinkDarker; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 00eac4adca..5142f125ac 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.MathUtils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Judgements; using OpenTK; using OpenTK.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; @@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables foreach (var tick in drumRoll.NestedHitObjects.OfType()) { var newTick = new DrawableDrumRollTick(tick); - newTick.OnJudgement += onTickJudgement; + newTick.OnNewResult += onNewTickResult; AddNested(newTick); tickContainer.Add(newTick); @@ -61,9 +60,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables colourEngaged = colours.YellowDarker; } - private void onTickJudgement(DrawableHitObject obj, Judgement judgement) + private void onNewTickResult(DrawableHitObject obj, JudgementResult result) { - if (judgement.Result > HitResult.Miss) + if (result.Type > HitResult.Miss) rollingHits++; else rollingHits--; @@ -74,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.FadeAccent(newColour, 100); } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (userTriggered) return; @@ -84,13 +83,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int countHit = NestedHitObjects.Count(o => o.IsHit); if (countHit >= HitObject.RequiredGoodHits) - { - AddJudgement(new TaikoJudgement { Result = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good }); - if (HitObject.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); - } + ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); else - AddJudgement(new TaikoJudgement { Result = HitResult.Miss }); + ApplyResult(r => r.Type = HitResult.Miss); } protected override void UpdateState(ArmedState state) @@ -103,5 +98,25 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables break; } } + + protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + + private class StrongNestedHit : DrawableStrongNestedHit + { + public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll) + : base(strong, drumRoll) + { + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (!MainObject.Judged) + return; + + ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss); + } + + public override bool OnPressed(TaikoAction action) => false; + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 7a57cf77b4..a70d7bde0e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -18,24 +17,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables FillMode = FillMode.Fit; } - public override bool DisplayJudgement => false; + public override bool DisplayResult => false; protected override TaikoPiece CreateMainPiece() => new TickPiece { Filled = HitObject.FirstTick }; - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) + { + if (timeOffset > HitObject.HitWindow) + ApplyResult(r => r.Type = HitResult.Miss); + return; + } + + if (Math.Abs(timeOffset) > HitObject.HitWindow) return; - if (!(Math.Abs(timeOffset) < HitObject.HitWindow)) - return; - - AddJudgement(new TaikoDrumRollTickJudgement { Result = HitResult.Great }); - if (HitObject.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); + ApplyResult(r => r.Type = HitResult.Great); } protected override void UpdateState(ArmedState state) @@ -48,6 +49,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - public override bool OnPressed(TaikoAction action) => UpdateJudgement(true); + public override bool OnPressed(TaikoAction action) => UpdateResult(true); + + protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + + private class StrongNestedHit : DrawableStrongNestedHit + { + public StrongNestedHit(StrongHitObject strong, DrawableDrumRollTick tick) + : base(strong, tick) + { + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (!MainObject.Judged) + return; + + ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss); + } + + public override bool OnPressed(TaikoAction action) => false; + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index bb9cd02b14..f59dc8c1ee 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -1,11 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -15,17 +15,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// A list of keys which can result in hits for this HitObject. /// - protected abstract TaikoAction[] HitActions { get; } + public abstract TaikoAction[] HitActions { get; } /// - /// Whether a second hit is allowed to be processed. This occurs once this hit object has been hit successfully. + /// The action that caused this to be hit. /// - protected bool SecondHitAllowed { get; private set; } + public TaikoAction? HitAction { get; private set; } - /// - /// Whether the last key pressed is a valid hit key. - /// - private bool validKeyPressed; + private bool validActionPressed; protected DrawableHit(Hit hit) : base(hit) @@ -33,12 +30,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables FillMode = FillMode.Fit; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - AddJudgement(new TaikoJudgement { Result = HitResult.Miss }); + ApplyResult(r => r.Type = HitResult.Miss); return; } @@ -46,26 +43,33 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (result == HitResult.None) return; - if (!validKeyPressed || result == HitResult.Miss) - AddJudgement(new TaikoJudgement { Result = HitResult.Miss }); + if (!validActionPressed) + ApplyResult(r => r.Type = HitResult.Miss); else - { - AddJudgement(new TaikoJudgement - { - Result = result, - Final = !HitObject.IsStrong - }); - - SecondHitAllowed = true; - } + ApplyResult(r => r.Type = result); } public override bool OnPressed(TaikoAction action) { - validKeyPressed = HitActions.Contains(action); + if (Judged) + return false; + + validActionPressed = HitActions.Contains(action); // Only count this as handled if the new judgement is a hit - return UpdateJudgement(true); + var result = UpdateResult(true); + + if (IsHit) + HitAction = action; + + return result; + } + + public override bool OnReleased(TaikoAction action) + { + if (action == HitAction) + HitAction = null; + return base.OnReleased(action); } protected override void Update() @@ -86,8 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (State.Value) { case ArmedState.Idle: - SecondHitAllowed = false; - validKeyPressed = false; + validActionPressed = false; UnproxyContent(); this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire(); @@ -123,5 +126,65 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } } + + protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + + private class StrongNestedHit : DrawableStrongNestedHit + { + /// + /// The lenience for the second key press. + /// This does not adjust by map difficulty in ScoreV2 yet. + /// + private const double second_hit_window = 30; + + public new DrawableHit MainObject => (DrawableHit)base.MainObject; + + public StrongNestedHit(StrongHitObject strong, DrawableHit hit) + : base(strong, hit) + { + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (!MainObject.Result.HasResult) + { + base.CheckForResult(userTriggered, timeOffset); + return; + } + + if (!MainObject.Result.IsHit) + { + ApplyResult(r => r.Type = HitResult.Miss); + return; + } + + if (!userTriggered) + { + if (timeOffset > second_hit_window) + ApplyResult(r => r.Type = HitResult.Miss); + return; + } + + if (Math.Abs(MainObject.Result.TimeOffset - timeOffset) < second_hit_window) + ApplyResult(r => r.Type = HitResult.Great); + } + + public override bool OnPressed(TaikoAction action) + { + // Don't process actions until the main hitobject is hit + if (!MainObject.IsHit) + return false; + + // Don't process actions if the pressed button was released + if (MainObject.HitAction == null) + return false; + + // Don't handle invalid hit action presses + if (!MainObject.HitActions.Contains(action)) + return false; + + return UpdateResult(true); + } + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs deleted file mode 100644 index b431d35e16..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Linq; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Judgements; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public abstract class DrawableHitStrong : DrawableHit - { - /// - /// The lenience for the second key press. - /// This does not adjust by map difficulty in ScoreV2 yet. - /// - private const double second_hit_window = 30; - - private double firstHitTime; - private bool firstKeyHeld; - private TaikoAction firstHitAction; - - protected DrawableHitStrong(Hit hit) - : base(hit) - { - } - - protected override void CheckForJudgements(bool userTriggered, double timeOffset) - { - if (!SecondHitAllowed) - { - base.CheckForJudgements(userTriggered, timeOffset); - return; - } - - if (!userTriggered) - { - if (timeOffset > second_hit_window) - AddJudgement(new TaikoStrongHitJudgement { Result = HitResult.None }); - return; - } - - // If we get here, we're assured that the key pressed is the correct secondary key - - if (Math.Abs(firstHitTime - Time.Current) < second_hit_window) - AddJudgement(new TaikoStrongHitJudgement { Result = HitResult.Great }); - } - - protected override void UpdateState(ArmedState state) - { - base.UpdateState(state); - - switch (state) - { - case ArmedState.Idle: - firstHitTime = 0; - firstKeyHeld = false; - break; - } - } - - public override bool OnReleased(TaikoAction action) - { - if (action == firstHitAction) - firstKeyHeld = false; - return base.OnReleased(action); - } - - public override bool OnPressed(TaikoAction action) - { - if (AllJudged) - return false; - - // Check if we've handled the first key - if (!SecondHitAllowed) - { - // First key hasn't been handled yet, attempt to handle it - bool handled = base.OnPressed(action); - - if (handled) - { - firstHitTime = Time.Current; - firstHitAction = action; - firstKeyHeld = true; - } - - return handled; - } - - // Don't handle represses of the first key - if (firstHitAction == action) - return false; - - // Don't handle invalid hit action presses - if (!HitActions.Contains(action)) - return false; - - // Assume the intention was to hit the strong hit with both keys only if the first key is still being held down - return firstKeyHeld && UpdateJudgement(true); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index f2194c6d56..188cafe1db 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableRimHit : DrawableHit { - protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; + public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; public DrawableRimHit(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs deleted file mode 100644 index 728fe416f7..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Game.Graphics; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public class DrawableRimHitStrong : DrawableHitStrong - { - protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; - - public DrawableRimHitStrong(Hit hit) - : base(hit) - { - MainPiece.Add(new RimHitSymbolPiece()); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - MainPiece.AccentColour = colours.BlueDarker; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs new file mode 100644 index 0000000000..b27de3832a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Judgements; + +namespace osu.Game.Rulesets.Taiko.Objects.Drawables +{ + /// + /// Used as a nested hitobject to provide s for s. + /// + public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject + { + public readonly DrawableHitObject MainObject; + + protected DrawableStrongNestedHit(StrongHitObject strong, DrawableHitObject mainObject) + : base(strong) + { + MainObject = mainObject; + } + + protected override void UpdateState(ArmedState state) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 408b37e377..5059734663 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -12,23 +14,19 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableSwell : DrawableTaikoHitObject { - /// - /// A judgement is only displayed when the user has complete the swell (either a hit or miss). - /// - public override bool DisplayJudgement => AllJudged; - private const float target_ring_thick_border = 1.4f; private const float target_ring_thin_border = 1f; private const float target_ring_scale = 5f; private const float inner_ring_alpha = 0.65f; + private readonly List ticks = new List(); + private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; @@ -106,6 +104,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables }); MainPiece.Add(symbol = new SwellSymbolPiece()); + + foreach (var tick in HitObject.NestedHitObjects.OfType()) + { + var vis = new DrawableSwellTick(tick); + + ticks.Add(vis); + AddInternal(vis); + AddNested(vis); + } } [BackgroundDependencyLoader] @@ -124,13 +131,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Width *= Parent.RelativeChildSize.X; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (userTriggered) { - AddJudgement(new TaikoIntermediateSwellJudgement()); + var nextTick = ticks.FirstOrDefault(j => !j.IsHit); - var completion = (float)Judgements.Count / HitObject.RequiredHits; + nextTick?.TriggerResult(HitResult.Great); + + var numHits = ticks.Count(r => r.IsHit); + + var completion = (float)numHits / HitObject.RequiredHits; expandingRing .FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50) @@ -141,18 +152,30 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); - if (Judgements.Count == HitObject.RequiredHits) - AddJudgement(new TaikoJudgement { Result = HitResult.Great }); + if (numHits == HitObject.RequiredHits) + ApplyResult(r => r.Type = HitResult.Great); } else { if (timeOffset < 0) return; - //TODO: THIS IS SHIT AND CAN'T EXIST POST-TAIKO WORLD CUP - AddJudgement(Judgements.Count > HitObject.RequiredHits / 2 - ? new TaikoJudgement { Result = HitResult.Good } - : new TaikoJudgement { Result = HitResult.Miss }); + int numHits = 0; + + foreach (var tick in ticks) + { + if (tick.IsHit) + { + numHits++; + continue; + } + + tick.TriggerResult(HitResult.Miss); + } + + var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Good : HitResult.Miss; + + ApplyResult(r => r.Type = hitResult); } } @@ -208,7 +231,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return false; lastWasCentre = isCentre; - UpdateJudgement(true); + UpdateResult(true); return true; } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs new file mode 100644 index 0000000000..36c468c6d6 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Taiko.Objects.Drawables +{ + public class DrawableSwellTick : DrawableTaikoHitObject + { + public override bool DisplayResult => false; + + public DrawableSwellTick(TaikoHitObject hitObject) + : base(hitObject) + { + } + + public void TriggerResult(HitResult type) => ApplyResult(r => r.Type = type); + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + } + + protected override void UpdateState(ArmedState state) + { + } + + public override bool OnPressed(TaikoAction action) => false; + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index a6d61f1a5a..51e39dc648 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -101,6 +101,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Content.Add(MainPiece = CreateMainPiece()); MainPiece.KiaiMode = HitObject.Kiai; + + var strongObject = HitObject.NestedHitObjects.OfType().FirstOrDefault(); + if (strongObject != null) + { + var strongHit = CreateStrongHit(strongObject); + + AddNested(strongHit); + AddInternal(strongHit); + } } // Normal and clap samples are handled by the drum @@ -109,5 +118,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override string SampleNamespace => "Taiko"; protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(); + + /// + /// Creates the handler for this 's . + /// This is only invoked if is true for . + /// + /// The strong hitobject. + /// The strong hitobject handler. + protected virtual DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => null; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 4c9ec5473b..405ea85f0d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -54,12 +54,12 @@ namespace osu.Game.Rulesets.Taiko.Objects protected override void CreateNestedHitObjects() { - base.CreateNestedHitObjects(); - createTicks(); RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty); RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty); + + base.CreateNestedHitObjects(); } private void createTicks() diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index e546d6427f..967d5acfd7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -1,6 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Taiko.Judgements; + namespace osu.Game.Rulesets.Taiko.Objects { public class DrumRollTick : TaikoHitObject @@ -20,5 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// The time allowed to hit this tick. /// public double HitWindow => TickSpacing / 2; + + public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs new file mode 100644 index 0000000000..fac3705110 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Taiko.Judgements; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public class StrongHitObject : TaikoHitObject + { + public override Judgement CreateJudgement() => new TaikoStrongJudgement(); + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index eb6f931af4..702bf63bf5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Taiko.Objects @@ -15,5 +16,15 @@ namespace osu.Game.Rulesets.Taiko.Objects /// The number of hits required to complete the swell successfully. /// public int RequiredHits = 10; + + public override bool IsStrong { set => throw new NotSupportedException($"{nameof(Swell)} cannot be a strong hitobject."); } + + protected override void CreateNestedHitObjects() + { + base.CreateNestedHitObjects(); + + for (int i = 0; i < RequiredHits; i++) + AddNested(new SwellTick()); + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs new file mode 100644 index 0000000000..49eb6d2a15 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs @@ -0,0 +1,9 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public class SwellTick : TaikoHitObject + { + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index ffbbe28f2e..9c86b60688 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -1,7 +1,10 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects { @@ -26,7 +29,17 @@ namespace osu.Game.Rulesets.Taiko.Objects /// Whether this HitObject is a "strong" type. /// Strong hit objects give more points for hitting the hit object with both keys. /// - public bool IsStrong; + public virtual bool IsStrong { get; set; } + + protected override void CreateNestedHitObjects() + { + base.CreateNestedHitObjects(); + + if (IsStrong) + AddNested(new StrongHitObject { StartTime = (this as IHasEndTime)?.EndTime ?? StartTime }); + } + + public override Judgement CreateJudgement() => new TaikoJudgement(); protected override HitWindows CreateHitWindows() => new TaikoHitWindows(); } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 7dd50ab8b8..cf33141027 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -60,63 +59,31 @@ namespace osu.Game.Rulesets.Taiko.Scoring private double hpIncreaseGood; private double hpIncreaseMiss; - public TaikoScoreProcessor() - { - } - public TaikoScoreProcessor(RulesetContainer rulesetContainer) : base(rulesetContainer) { } - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void ApplyBeatmap(Beatmap beatmap) { + base.ApplyBeatmap(beatmap); + double hpMultiplierNormal = 1 / (hp_hit_great * beatmap.HitObjects.FindAll(o => o is Hit).Count * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); hpIncreaseTick = hp_hit_tick; hpIncreaseGreat = hpMultiplierNormal * hp_hit_great; hpIncreaseGood = hpMultiplierNormal * hp_hit_good; hpIncreaseMiss = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, hp_miss_min, hp_miss_mid, hp_miss_max); - - foreach (var obj in beatmap.HitObjects) - { - switch (obj) - { - case Hit _: - AddJudgement(new TaikoJudgement { Result = HitResult.Great }); - if (obj.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); - break; - case DrumRoll drumRoll: - var count = drumRoll.NestedHitObjects.OfType().Count(); - for (int i = 0; i < count; i++) - { - AddJudgement(new TaikoDrumRollTickJudgement { Result = HitResult.Great }); - - if (obj.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); - } - - AddJudgement(new TaikoJudgement { Result = HitResult.Great }); - - if (obj.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); - break; - case Swell _: - AddJudgement(new TaikoJudgement { Result = HitResult.Great }); - break; - } - } } - protected override void OnNewJudgement(Judgement judgement) + protected override void ApplyResult(JudgementResult result) { - base.OnNewJudgement(judgement); + base.ApplyResult(result); - bool isTick = judgement is TaikoDrumRollTickJudgement; + bool isTick = result.Judgement is TaikoDrumRollTickJudgement; // Apply HP changes - switch (judgement.Result) + switch (result.Type) { case HitResult.Miss: // Missing ticks shouldn't drop HP diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index b07a3ce8df..4d660918b8 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -19,16 +19,16 @@ namespace osu.Game.Rulesets.Taiko.UI /// Creates a new judgement text. /// /// The object which is being judged. - /// The judgement to visualise. - public DrawableTaikoJudgement(Judgement judgement, DrawableHitObject judgedObject) - : base(judgement, judgedObject) + /// The judgement to visualise. + public DrawableTaikoJudgement(JudgementResult result, DrawableHitObject judgedObject) + : base(result, judgedObject) { } [BackgroundDependencyLoader] private void load(OsuColour colours) { - switch (Judgement.Result) + switch (Result.Type) { case HitResult.Good: Colour = colours.GreenLight; @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected override void LoadComplete() { - if (Judgement.IsHit) + if (Result.IsHit) this.MoveToY(-100, 500); base.LoadComplete(); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 4cb8dd48a7..325beb38a5 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Taiko.UI public override void Add(DrawableHitObject h) { - h.OnJudgement += OnJudgement; + h.OnNewResult += OnNewResult; base.Add(h); @@ -224,35 +224,40 @@ namespace osu.Game.Rulesets.Taiko.UI } } - internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!DisplayJudgements) return; - if (judgedObject.DisplayJudgement && judgementContainer.FirstOrDefault(j => j.JudgedObject == judgedObject) == null) - { - judgementContainer.Add(new DrawableTaikoJudgement(judgement, judgedObject) - { - Anchor = judgement.IsHit ? Anchor.TopLeft : Anchor.CentreLeft, - Origin = judgement.IsHit ? Anchor.BottomCentre : Anchor.Centre, - RelativePositionAxes = Axes.X, - X = judgement.IsHit ? judgedObject.Position.X : 0, - }); - } - - if (!judgement.IsHit) + if (!judgedObject.DisplayResult) return; - bool isRim = judgedObject.HitObject is RimHit; - - if (judgement is TaikoStrongHitJudgement) - hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == judgedObject)?.VisualiseSecondHit(); - else + switch (result.Judgement) { - hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim)); + case TaikoStrongJudgement _: + if (result.IsHit) + hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit(); + break; + default: + judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject) + { + Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft, + Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre, + RelativePositionAxes = Axes.X, + X = result.IsHit ? judgedObject.Position.X : 0, + }); - if (judgedObject.HitObject.Kiai) - kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim)); + if (!result.IsHit) + break; + + bool isRim = judgedObject.HitObject is RimHit; + + hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim)); + + if (judgedObject.HitObject.Kiai) + kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim)); + + break; } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 2fa4627bde..229ab69ceb 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -100,12 +100,8 @@ namespace osu.Game.Rulesets.Taiko.UI { switch (h) { - case CentreHit centreHit when h.IsStrong: - return new DrawableCentreHitStrong(centreHit); case CentreHit centreHit: return new DrawableCentreHit(centreHit); - case RimHit rimHit when h.IsStrong: - return new DrawableRimHitStrong(rimHit); case RimHit rimHit: return new DrawableRimHit(rimHit); case DrumRoll drumRoll: diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 400380b407..d3351f86f8 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -11,7 +11,9 @@ using osu.Game.Audio; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Skinning; namespace osu.Game.Tests.Beatmaps.Formats @@ -186,6 +188,50 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeBeatmapComboOffsetsOsu() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("hitobject-combo-offset.osu")) + using (var stream = new StreamReader(resStream)) + { + var beatmap = decoder.Decode(stream); + + var converted = new OsuBeatmapConverter(beatmap).Convert(); + new OsuBeatmapProcessor(converted).PreProcess(); + new OsuBeatmapProcessor(converted).PostProcess(); + + Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex); + Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex); + Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex); + Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex); + Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex); + Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex); + } + } + + [Test] + public void TestDecodeBeatmapComboOffsetsCatch() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("hitobject-combo-offset.osu")) + using (var stream = new StreamReader(resStream)) + { + var beatmap = decoder.Decode(stream); + + var converted = new CatchBeatmapConverter(beatmap).Convert(); + new CatchBeatmapProcessor(converted).PreProcess(); + new CatchBeatmapProcessor(converted).PostProcess(); + + Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex); + Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex); + Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex); + Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex); + Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex); + Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex); + } + } + [Test] public void TestDecodeBeatmapHitObjects() { diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 3431be91f9..82adc88c6b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -86,5 +86,19 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(78993, animation.StartTime); } } + + [Test] + public void TestDecodeVariableWithSuffix() + { + var decoder = new LegacyStoryboardDecoder(); + using (var resStream = Resource.OpenResource("variable-with-suffix.osb")) + using (var stream = new StreamReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + Assert.AreEqual(123456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X); + } + } } } diff --git a/osu.Game.Tests/Resources/hitobject-combo-offset.osu b/osu.Game.Tests/Resources/hitobject-combo-offset.osu new file mode 100644 index 0000000000..c1f0dab8e9 --- /dev/null +++ b/osu.Game.Tests/Resources/hitobject-combo-offset.osu @@ -0,0 +1,32 @@ +osu file format v14 + +[HitObjects] +// Circle with combo offset (3) +255,193,1000,49,0,0:0:0:0: +// Combo index = 4 + +// Slider with new combo followed by circle with no new combo +256,192,2000,12,0,2000,0:0:0:0: +255,193,3000,1,0,0:0:0:0: +// Combo index = 5 + +// Slider without new combo followed by circle with no new combo +256,192,4000,8,0,5000,0:0:0:0: +255,193,6000,1,0,0:0:0:0: +// Combo index = 5 + +// Slider without new combo followed by circle with new combo +256,192,7000,8,0,8000,0:0:0:0: +255,193,9000,5,0,0:0:0:0: +// Combo index = 6 + +// Slider with new combo and offset (1) followed by circle with new combo and offset (3) +256,192,10000,28,0,11000,0:0:0:0: +255,193,12000,53,0,0:0:0:0: +// Combo index = 11 + +// Slider with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo +256,192,13000,44,0,14000,0:0:0:0: +256,192,15000,8,0,16000,0:0:0:0: +255,193,17000,1,0,0:0:0:0: +// Combo index = 14 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/variable-with-suffix.osb b/osu.Game.Tests/Resources/variable-with-suffix.osb new file mode 100644 index 0000000000..5c9b46ca98 --- /dev/null +++ b/osu.Game.Tests/Resources/variable-with-suffix.osb @@ -0,0 +1,5 @@ +[Variables] +$var=1234 + +[Events] +Sprite,Background,TopCentre,"img.jpg",$var56,240 diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs index b232180eba..175db7d246 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using OpenTK; using osu.Framework.Allocation; @@ -116,7 +117,7 @@ namespace osu.Game.Tests.Visual private void testNullBeatmap() { - selectNullBeatmap(); + selectBeatmap(null); AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text)); AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title); AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist); @@ -124,28 +125,19 @@ namespace osu.Game.Tests.Visual AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any()); } - private void selectBeatmap(IBeatmap b) + private void selectBeatmap([CanBeNull] IBeatmap b) { BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null; - AddStep($"select {b.Metadata.Title} beatmap", () => + AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => { infoBefore = infoWedge.Info; - infoWedge.Beatmap = Beatmap.Value = new TestWorkingBeatmap(b); + infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : new TestWorkingBeatmap(b); }); AddUntilStep(() => infoWedge.Info != infoBefore, "wait for async load"); } - private void selectNullBeatmap() - { - AddStep("select null beatmap", () => - { - Beatmap.Value = Beatmap.Default; - infoWedge.Beatmap = Beatmap; - }); - } - private IBeatmap createTestBeatmap(RulesetInfo ruleset) { List objects = new List(); diff --git a/osu.Game.Tests/Visual/TestCaseMods.cs b/osu.Game.Tests/Visual/TestCaseMods.cs index cc396a63e3..ab53dbd968 100644 --- a/osu.Game.Tests/Visual/TestCaseMods.cs +++ b/osu.Game.Tests/Visual/TestCaseMods.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays.Mods; @@ -13,11 +12,11 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using System.Linq; using System.Collections.Generic; -using osu.Game.Rulesets.Osu; +using NUnit.Framework; +using osu.Framework.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Mods.Sections; -using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.UI; using OpenTK.Graphics; @@ -50,11 +49,6 @@ namespace osu.Game.Tests.Visual private void load(RulesetStore rulesets) { this.rulesets = rulesets; - } - - protected override void LoadComplete() - { - base.LoadComplete(); Add(modSelect = new TestModSelectOverlay { @@ -71,34 +65,25 @@ namespace osu.Game.Tests.Visual Position = new Vector2(0, 25), }); + modDisplay.Current.UnbindBindings(); modDisplay.Current.BindTo(modSelect.SelectedMods); - AddStep("Toggle", modSelect.ToggleVisibility); - AddStep("Hide", modSelect.Hide); AddStep("Show", modSelect.Show); - - foreach (var rulesetInfo in rulesets.AvailableRulesets) - { - Ruleset ruleset = rulesetInfo.CreateInstance(); - AddStep($"switch to {ruleset.Description}", () => Ruleset.Value = rulesetInfo); - - switch (ruleset) - { - case OsuRuleset or: - testOsuMods(or); - break; - case ManiaRuleset mr: - testManiaMods(mr); - break; - } - } + AddStep("Toggle", modSelect.ToggleVisibility); + AddStep("Toggle", modSelect.ToggleVisibility); } - private void testOsuMods(OsuRuleset ruleset) + [Test] + public void TestOsuMods() { - var easierMods = ruleset.GetModsFor(ModType.DifficultyReduction); - var harderMods = ruleset.GetModsFor(ModType.DifficultyIncrease); - var assistMods = ruleset.GetModsFor(ModType.Automation); + var ruleset = rulesets.AvailableRulesets.First(r => r.ID == 0); + AddStep("change ruleset", () => { Ruleset.Value = ruleset; }); + + var instance = ruleset.CreateInstance(); + + var easierMods = instance.GetModsFor(ModType.DifficultyReduction); + var harderMods = instance.GetModsFor(ModType.DifficultyIncrease); + var assistMods = instance.GetModsFor(ModType.Automation); var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail); var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); @@ -120,9 +105,40 @@ namespace osu.Game.Tests.Visual testUnimplementedMod(autoPilotMod); } - private void testManiaMods(ManiaRuleset ruleset) + [Test] + public void TestManiaMods() { - testRankedText(ruleset.GetModsFor(ModType.Conversion).First(m => m is ManiaModRandom)); + var ruleset = rulesets.AvailableRulesets.First(r => r.ID == 3); + AddStep("change ruleset", () => { Ruleset.Value = ruleset; }); + + testRankedText(ruleset.CreateInstance().GetModsFor(ModType.Conversion).First(m => m is ManiaModRandom)); + } + + [Test] + public void TestRulesetChanges() + { + var rulesetOsu = rulesets.AvailableRulesets.First(r => r.ID == 0); + var rulesetMania = rulesets.AvailableRulesets.First(r => r.ID == 3); + + AddStep("change ruleset to null", () => { Ruleset.Value = null; }); + + var instance = rulesetOsu.CreateInstance(); + var easierMods = instance.GetModsFor(ModType.DifficultyReduction); + var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail); + + AddStep("set mods externally", () => { modDisplay.Current.Value = new[] { noFailMod }; }); + + AddStep("change ruleset to osu", () => { Ruleset.Value = rulesetOsu; }); + + AddAssert("ensure mods still selected", () => modDisplay.Current.Value.Single(m => m is OsuModNoFail) != null); + + AddStep("change ruleset to mania", () => { Ruleset.Value = rulesetMania; }); + + AddAssert("ensure mods not selected", () => !modDisplay.Current.Value.Any(m => m is OsuModNoFail)); + + AddStep("change ruleset to osu", () => { Ruleset.Value = rulesetOsu; }); + + AddAssert("ensure mods not selected", () => !modDisplay.Current.Value.Any()); } private void testSingleMod(Mod mod) @@ -237,6 +253,8 @@ namespace osu.Game.Tests.Visual private class TestModSelectOverlay : ModSelectOverlay { + public new Bindable> SelectedMods => base.SelectedMods; + public ModButton GetModButton(Mod mod) { var section = ModSectionsContainer.Children.Single(s => s.ModType == mod.Type); diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index b1ffe04b68..888bf6250f 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -8,11 +8,15 @@ using System.Linq; using System.Text; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions; using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; @@ -29,6 +33,10 @@ namespace osu.Game.Tests.Visual private WorkingBeatmap defaultBeatmap; private DatabaseContextFactory factory; + [Cached] + [Cached(Type = typeof(IBindable>))] + private readonly Bindable> selectedMods = new Bindable>(new Mod[] { }); + public override IReadOnlyList RequiredTypes => new[] { typeof(SongSelect), @@ -49,6 +57,8 @@ namespace osu.Game.Tests.Visual private class TestSongSelect : PlaySongSelect { + public new Bindable Ruleset => base.Ruleset; + public WorkingBeatmap CurrentBeatmap => Beatmap.Value; public WorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap; public new BeatmapCarousel Carousel => base.Carousel; @@ -121,7 +131,7 @@ namespace osu.Game.Tests.Visual [Test] [Ignore("needs fixing")] - public void ImportUnderDifferentRuleset() + public void TestImportUnderDifferentRuleset() { changeRuleset(2); importForRuleset(0); @@ -129,7 +139,7 @@ namespace osu.Game.Tests.Visual } [Test] - public void ImportUnderCurrentRuleset() + public void TestImportUnderCurrentRuleset() { changeRuleset(2); importForRuleset(2); @@ -143,11 +153,42 @@ namespace osu.Game.Tests.Visual AddUntilStep(() => songSelect.Carousel.SelectedBeatmap == null, "no selection"); } + [Test] + public void TestRulesetChangeResetsMods() + { + changeRuleset(0); + + changeMods(new OsuModHardRock()); + + int actionIndex = 0; + int modChangeIndex = 0; + int rulesetChangeIndex = 0; + + AddStep("change ruleset", () => + { + songSelect.CurrentBeatmap.Mods.ValueChanged += onModChange; + songSelect.Ruleset.ValueChanged += onRulesetChange; + + Ruleset.Value = new TaikoRuleset().RulesetInfo; + + songSelect.CurrentBeatmap.Mods.ValueChanged -= onModChange; + songSelect.Ruleset.ValueChanged -= onRulesetChange; + }); + + AddAssert("mods changed before ruleset", () => modChangeIndex < rulesetChangeIndex); + AddAssert("empty mods", () => !selectedMods.Value.Any()); + + void onModChange(IEnumerable mods) => modChangeIndex = actionIndex++; + void onRulesetChange(RulesetInfo ruleset) => rulesetChangeIndex = actionIndex--; + } + private void importForRuleset(int id) => AddStep($"import test map for ruleset {id}", () => manager.Import(createTestBeatmapSet(getImportId(), rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray()))); private static int importId; private int getImportId() => ++importId; + private void changeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.ShortenedName))}", () => selectedMods.Value = mods); + private void changeRuleset(int id) => AddStep($"change ruleset to {id}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == id)); private void addManyTestMaps() diff --git a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs index 52a9db080d..de839a21af 100644 --- a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs +++ b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs @@ -1,25 +1,61 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Threading; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { - public class TestCasePlayerLoader : OsuTestCase + public class TestCasePlayerLoader : ManualInputManagerTestCase { + private PlayerLoader loader; + [BackgroundDependencyLoader] private void load(OsuGameBase game) { Beatmap.Value = new DummyWorkingBeatmap(game); - AddStep("load dummy beatmap", () => Add(new PlayerLoader(new Player + AddStep("load dummy beatmap", () => Add(loader = new PlayerLoader(new Player { AllowPause = false, AllowLeadIn = false, AllowResults = false, }))); + + AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); + + AddUntilStep(() => !loader.IsCurrentScreen, "wait for no longer current"); + + AddStep("load slow dummy beatmap", () => + { + SlowLoadPlayer slow; + + Add(loader = new PlayerLoader(slow = new SlowLoadPlayer + { + AllowPause = false, + AllowLeadIn = false, + AllowResults = false, + })); + + Scheduler.AddDelayed(() => slow.Ready = true, 5000); + }); + + AddUntilStep(() => !loader.IsCurrentScreen, "wait for no longer current"); + } + + protected class SlowLoadPlayer : Player + { + public bool Ready; + + [BackgroundDependencyLoader] + private void load() + { + while (!Ready) + Thread.Sleep(1); + } } } + } diff --git a/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs b/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs index e4cb848d90..041fce6ce3 100644 --- a/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs +++ b/osu.Game.Tests/Visual/TestCasePreviewTrackManager.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { - AddInternal(trackManager); + Add(trackManager); } [Test] @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual TestTrackOwner owner = null; PreviewTrack track = null; - AddStep("get track", () => AddInternal(owner = new TestTrackOwner(track = getTrack()))); + AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack()))); AddStep("start", () => track.Start()); AddStep("attempt stop", () => trackManager.StopAnyPlaying(this)); AddAssert("not stopped", () => track.IsRunning); @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual { var track = getTrack(); - AddInternal(track); + Add(track); return track; } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 67f02c8ac4..21df9a6c68 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Framework.Platform; @@ -62,6 +63,8 @@ namespace osu.Game.Beatmaps public override string[] HandledExtensions => new[] { ".osz" }; + protected override string ImportFromStablePath => "Songs"; + private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; @@ -72,11 +75,6 @@ namespace osu.Game.Beatmaps private readonly List currentDownloads = new List(); - /// - /// Set a storage with access to an osu-stable install for import purposes. - /// - public Func GetStableStorage { private get; set; } - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null) : base(storage, contextFactory, new BeatmapStore(contextFactory), importHost) { @@ -103,6 +101,11 @@ namespace osu.Game.Beatmaps b.BeatmapSet = beatmapSet; } + validateOnlineIds(beatmapSet.Beatmaps); + + foreach (BeatmapInfo b in beatmapSet.Beatmaps) + fetchAndPopulateOnlineIDs(b, beatmapSet.Beatmaps); + // check if a set already exists with the same online id, delete if it does. if (beatmapSet.OnlineBeatmapSetID != null) { @@ -114,11 +117,6 @@ namespace osu.Game.Beatmaps Logger.Log($"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged.", LoggingTarget.Database); } } - - validateOnlineIds(beatmapSet.Beatmaps); - - foreach (BeatmapInfo b in beatmapSet.Beatmaps) - fetchAndPopulateOnlineIDs(b, beatmapSet.Beatmaps); } private void validateOnlineIds(List beatmaps) @@ -195,7 +193,7 @@ namespace osu.Game.Beatmaps downloadNotification.CompletionClickAction = () => { - PresentBeatmap?.Invoke(importedBeatmap); + PresentCompletedImport(importedBeatmap.Yield()); return true; }; downloadNotification.State = ProgressNotificationState.Completed; @@ -231,6 +229,12 @@ namespace osu.Game.Beatmaps BeatmapDownloadBegan?.Invoke(request); } + protected override void PresentCompletedImport(IEnumerable imported) + { + base.PresentCompletedImport(imported); + PresentBeatmap?.Invoke(imported.LastOrDefault()); + } + /// /// Get an existing download request if it exists. /// @@ -311,27 +315,6 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); - /// - /// Denotes whether an osu-stable installation is present to perform automated imports from. - /// - public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; - - /// - /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. - /// - public async Task ImportFromStable() - { - var stable = GetStableStorage?.Invoke(); - - if (stable == null) - { - Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); - return; - } - - await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); - } - /// /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. /// @@ -350,7 +333,11 @@ namespace osu.Game.Beatmaps { // let's make sure there are actually .osu files to import. string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); - if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in this beatmap archive."); + if (string.IsNullOrEmpty(mapName)) + { + Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database); + return null; + } Beatmap beatmap; using (var stream = new StreamReader(reader.GetStream(mapName))) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 43ae30f780..77ff53b893 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Game.Beatmaps.Formats; -using osu.Game.Graphics.Textures; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -45,6 +44,10 @@ namespace osu.Game.Beatmaps private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; + private LargeTextureStore textureStore; + + protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. + protected override Texture GetBackground() { if (Metadata?.BackgroundFile == null) @@ -52,7 +55,7 @@ namespace osu.Game.Beatmaps try { - return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile)); + return (textureStore ?? (textureStore = new LargeTextureStore(new TextureLoaderStore(store)))).Get(getPathForFile(Metadata.BackgroundFile)); } catch { @@ -73,6 +76,14 @@ namespace osu.Game.Beatmaps } } + public override void TransferTo(WorkingBeatmap other) + { + base.TransferTo(other); + + if (other is BeatmapManagerWorkingBeatmap owb && textureStore != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) + owb.textureStore = textureStore; + } + protected override Waveform GetWaveform() { try diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs index 0173125e8b..9d7cd673dc 100644 --- a/osu.Game/Beatmaps/BeatmapProcessor.cs +++ b/osu.Game/Beatmaps/BeatmapProcessor.cs @@ -27,11 +27,10 @@ namespace osu.Game.Beatmaps if (obj.NewCombo) { obj.IndexInCurrentCombo = 0; + obj.ComboIndex = (lastObj?.ComboIndex ?? 0) + obj.ComboOffset + 1; + if (lastObj != null) - { lastObj.LastInCombo = true; - obj.ComboIndex = lastObj.ComboIndex + 1; - } } else if (lastObj != null) { diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs index 2927654f62..6f45718390 100644 --- a/osu.Game/Beatmaps/Formats/Decoder.cs +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -55,11 +55,11 @@ namespace osu.Game.Beatmaps.Formats } while (line != null && line.Length == 0); if (line == null) - throw new IOException(@"Unknown file format"); + throw new IOException(@"Unknown file format (null)"); - var decoder = typedDecoders.Select(d => line.StartsWith(d.Key) ? d.Value : null).FirstOrDefault(); + var decoder = typedDecoders.Select(d => line.StartsWith(d.Key, StringComparison.InvariantCulture) ? d.Value : null).FirstOrDefault(); if (decoder == null) - throw new IOException(@"Unknown file format"); + throw new IOException($@"Unknown file format ({line})"); return (Decoder)decoder.Invoke(line); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 26f28c86ca..181d17932d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -126,16 +126,16 @@ namespace osu.Game.Beatmaps.Formats switch (beatmap.BeatmapInfo.RulesetID) { case 0: - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); + parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); break; case 1: - parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(); + parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion); break; case 2: - parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(); + parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion); break; case 3: - parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(); + parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion); break; } @@ -354,6 +354,11 @@ namespace osu.Game.Beatmaps.Formats private void handleTimingControlPoint(TimingControlPoint newPoint) { + var existing = beatmap.ControlPointInfo.TimingPointAt(newPoint.Time); + + if (existing.Time == newPoint.Time) + beatmap.ControlPointInfo.TimingPoints.Remove(existing); + beatmap.ControlPointInfo.TimingPoints.Add(newPoint); } @@ -364,7 +369,9 @@ namespace osu.Game.Beatmaps.Formats if (newPoint.EquivalentTo(existing)) return; - beatmap.ControlPointInfo.DifficultyPoints.RemoveAll(x => x.Time == newPoint.Time); + if (existing.Time == newPoint.Time) + beatmap.ControlPointInfo.DifficultyPoints.Remove(existing); + beatmap.ControlPointInfo.DifficultyPoints.Add(newPoint); } @@ -375,6 +382,9 @@ namespace osu.Game.Beatmaps.Formats if (newPoint.EquivalentTo(existing)) return; + if (existing.Time == newPoint.Time) + beatmap.ControlPointInfo.EffectPoints.Remove(existing); + beatmap.ControlPointInfo.EffectPoints.Add(newPoint); } @@ -385,6 +395,9 @@ namespace osu.Game.Beatmaps.Formats if (newPoint.EquivalentTo(existing)) return; + if (existing.Time == newPoint.Time) + beatmap.ControlPointInfo.SamplePoints.Remove(existing); + beatmap.ControlPointInfo.SamplePoints.Add(newPoint); } @@ -392,14 +405,11 @@ namespace osu.Game.Beatmaps.Formats { // If the ruleset wasn't specified, assume the osu!standard ruleset. if (parser == null) - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); - - var obj = parser.Parse(line, getOffsetTime()); + parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); + var obj = parser.Parse(line); if (obj != null) - { beatmap.HitObjects.Add(obj); - } } private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0); diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 76a3d75e36..e9f37e583b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps.Formats if (ShouldSkipLine(line)) continue; - if (line.StartsWith(@"[") && line.EndsWith(@"]")) + if (line.StartsWith(@"[", StringComparison.Ordinal) && line.EndsWith(@"]", StringComparison.Ordinal)) { if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) { @@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps.Formats } } - protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.StartsWith("//"); + protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.StartsWith("//", StringComparison.Ordinal); protected virtual void ParseLine(T output, Section section, string line) { diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index b418cbd5ec..a73a32325a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats private void handleEvents(string line) { var depth = 0; - while (line.StartsWith(" ") || line.StartsWith("_")) + while (line.StartsWith(" ", StringComparison.Ordinal) || line.StartsWith("_", StringComparison.Ordinal)) { ++depth; line = line.Substring(1); @@ -269,9 +269,9 @@ namespace osu.Game.Beatmaps.Formats return Anchor.BottomCentre; case LegacyOrigins.BottomRight: return Anchor.BottomRight; + default: + return Anchor.TopLeft; } - - throw new InvalidDataException($@"Unknown origin: {value}"); } private void handleVariables(string line) @@ -289,15 +289,10 @@ namespace osu.Game.Beatmaps.Formats while (line.IndexOf('$') >= 0) { string origLine = line; - string[] split = line.Split(','); - for (int i = 0; i < split.Length; i++) - { - var item = split[i]; - if (item.StartsWith("$") && variables.ContainsKey(item)) - split[i] = variables[item]; - } - line = string.Join(",", split); + foreach (var v in variables) + line = line.Replace(v.Key, v.Value); + if (line == origLine) break; } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 6c906bb1e4..a2b44aab52 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -8,10 +8,10 @@ using osu.Game.Rulesets.Mods; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Game.Storyboards; using osu.Framework.IO.File; using System.IO; +using System.Threading; using osu.Game.IO.Serialization; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -38,12 +38,26 @@ namespace osu.Game.Beatmaps Mods.ValueChanged += mods => applyRateAdjustments(); - beatmap = new AsyncLazy(populateBeatmap); - background = new AsyncLazy(populateBackground, b => b == null || !b.IsDisposed); - track = new AsyncLazy(populateTrack); - waveform = new AsyncLazy(populateWaveform); - storyboard = new AsyncLazy(populateStoryboard); - skin = new AsyncLazy(populateSkin); + beatmap = new RecyclableLazy(() => + { + var b = GetBeatmap() ?? new Beatmap(); + // use the database-backed info. + b.BeatmapInfo = BeatmapInfo; + return b; + }); + + track = new RecyclableLazy(() => + { + // we want to ensure that we always have a track, even if it's a fake one. + var t = GetTrack() ?? new VirtualBeatmapTrack(Beatmap); + applyRateAdjustments(t); + return t; + }); + + background = new RecyclableLazy(GetBackground, BackgroundStillValid); + waveform = new RecyclableLazy(GetWaveform); + storyboard = new RecyclableLazy(GetStoryboard); + skin = new RecyclableLazy(GetSkin); } /// @@ -58,28 +72,6 @@ namespace osu.Game.Beatmaps return path; } - protected abstract IBeatmap GetBeatmap(); - protected abstract Texture GetBackground(); - protected abstract Track GetTrack(); - protected virtual Skin GetSkin() => new DefaultSkin(); - protected virtual Waveform GetWaveform() => new Waveform(); - protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; - - public bool BeatmapLoaded => beatmap.IsResultAvailable; - public IBeatmap Beatmap => beatmap.Value.Result; - public async Task GetBeatmapAsync() => await beatmap.Value; - private readonly AsyncLazy beatmap; - - private IBeatmap populateBeatmap() - { - var b = GetBeatmap() ?? new Beatmap(); - - // use the database-backed info. - b.BeatmapInfo = BeatmapInfo; - - return b; - } - /// /// Constructs a playable from using the applicable converters for a specific . /// @@ -136,62 +128,53 @@ namespace osu.Game.Beatmaps public override string ToString() => BeatmapInfo.ToString(); - public bool BackgroundLoaded => background.IsResultAvailable; - public Texture Background => background.Value.Result; - public async Task GetBackgroundAsync() => await background.Value; - private AsyncLazy background; + public bool BeatmapLoaded => beatmap.IsResultAvailable; + public IBeatmap Beatmap => beatmap.Value; + protected abstract IBeatmap GetBeatmap(); + private readonly RecyclableLazy beatmap; - private Texture populateBackground() => GetBackground(); + public bool BackgroundLoaded => background.IsResultAvailable; + public Texture Background => background.Value; + protected virtual bool BackgroundStillValid(Texture b) => b == null || !b.IsDisposed; + protected abstract Texture GetBackground(); + private readonly RecyclableLazy background; public bool TrackLoaded => track.IsResultAvailable; - public Track Track => track.Value.Result; - public async Task GetTrackAsync() => await track.Value; - private AsyncLazy track; - - private Track populateTrack() - { - // we want to ensure that we always have a track, even if it's a fake one. - var t = GetTrack() ?? new VirtualBeatmapTrack(Beatmap); - applyRateAdjustments(t); - return t; - } + public Track Track => track.Value; + protected abstract Track GetTrack(); + private RecyclableLazy track; public bool WaveformLoaded => waveform.IsResultAvailable; - public Waveform Waveform => waveform.Value.Result; - public async Task GetWaveformAsync() => await waveform.Value; - private readonly AsyncLazy waveform; - - private Waveform populateWaveform() => GetWaveform(); + public Waveform Waveform => waveform.Value; + protected virtual Waveform GetWaveform() => new Waveform(); + private readonly RecyclableLazy waveform; public bool StoryboardLoaded => storyboard.IsResultAvailable; - public Storyboard Storyboard => storyboard.Value.Result; - public async Task GetStoryboardAsync() => await storyboard.Value; - private readonly AsyncLazy storyboard; - - private Storyboard populateStoryboard() => GetStoryboard(); + public Storyboard Storyboard => storyboard.Value; + protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; + private readonly RecyclableLazy storyboard; public bool SkinLoaded => skin.IsResultAvailable; - public Skin Skin => skin.Value.Result; - public async Task GetSkinAsync() => await skin.Value; - private readonly AsyncLazy skin; + public Skin Skin => skin.Value; + protected virtual Skin GetSkin() => new DefaultSkin(); + private readonly RecyclableLazy skin; - private Skin populateSkin() => GetSkin(); - - public void TransferTo(WorkingBeatmap other) + /// + /// Transfer pieces of a beatmap to a new one, where possible, to save on loading. + /// + /// The new beatmap which is being switched to. + public virtual void TransferTo(WorkingBeatmap other) { if (track.IsResultAvailable && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) other.track = track; - - if (background.IsResultAvailable && Background != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) - other.background = background; } public virtual void Dispose() { - if (BackgroundLoaded) Background?.Dispose(); - if (WaveformLoaded) Waveform?.Dispose(); - if (StoryboardLoaded) Storyboard?.Dispose(); - if (SkinLoaded) Skin?.Dispose(); + background.Recycle(); + waveform.Recycle(); + storyboard.Recycle(); + skin.Recycle(); } /// @@ -210,15 +193,15 @@ namespace osu.Game.Beatmaps mod.ApplyToClock(t); } - public class AsyncLazy + public class RecyclableLazy { - private Lazy> lazy; + private Lazy lazy; private readonly Func valueFactory; private readonly Func stillValidFunction; - private readonly object initLock = new object(); + private readonly object fetchLock = new object(); - public AsyncLazy(Func valueFactory, Func stillValidFunction = null) + public RecyclableLazy(Func valueFactory, Func stillValidFunction = null) { this.valueFactory = valueFactory; this.stillValidFunction = stillValidFunction; @@ -230,45 +213,28 @@ namespace osu.Game.Beatmaps { if (!IsResultAvailable) return; - (lazy.Value.Result as IDisposable)?.Dispose(); + (lazy.Value as IDisposable)?.Dispose(); recreate(); } - public bool IsResultAvailable + public bool IsResultAvailable => stillValid; + + public T Value { get { - recreateIfInvalid(); - return lazy.Value.IsCompleted; + lock (fetchLock) + { + if (!stillValid) + recreate(); + return lazy.Value; + } } } - public Task Value - { - get - { - recreateIfInvalid(); - return lazy.Value; - } - } + private bool stillValid => lazy.IsValueCreated && (stillValidFunction?.Invoke(lazy.Value) ?? true); - private void recreateIfInvalid() - { - lock (initLock) - { - if (!lazy.IsValueCreated || !lazy.Value.IsCompleted) - // we have not yet been initialised or haven't run the task. - return; - - if (stillValidFunction?.Invoke(lazy.Value.Result) ?? true) - // we are still in a valid state. - return; - - recreate(); - } - } - - private void recreate() => lazy = new Lazy>(() => Task.Run(valueFactory)); + private void recreate() => lazy = new Lazy(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); } } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 0465c0ad73..e9fe943f15 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.IO.File; @@ -129,7 +130,6 @@ namespace osu.Game.Database List imported = new List(); int current = 0; - int errors = 0; foreach (string path in paths) { if (notification.State == ProgressNotificationState.Cancelled) @@ -162,12 +162,29 @@ namespace osu.Game.Database { e = e.InnerException ?? e; Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})"); - errors++; } } - notification.Text = errors > 0 ? $"Import complete with {errors} errors" : "Import successful!"; - notification.State = ProgressNotificationState.Completed; + if (imported.Count == 0) + { + notification.Text = "Import failed!"; + notification.State = ProgressNotificationState.Cancelled; + } + else + { + notification.CompletionText = $"Imported {current} {typeof(TModel).Name.Replace("Info", "").ToLower()}s!"; + notification.CompletionClickAction += () => + { + if (imported.Count > 0) + PresentCompletedImport(imported); + return true; + }; + notification.State = ProgressNotificationState.Completed; + } + } + + protected virtual void PresentCompletedImport(IEnumerable imported) + { } /// @@ -178,7 +195,8 @@ namespace osu.Game.Database { try { - return Import(CreateModel(archive), archive); + var model = CreateModel(archive); + return model == null ? null : Import(model, archive); } catch (Exception e) { @@ -198,6 +216,8 @@ namespace osu.Game.Database try { + Logger.Log($"Importing {item}...", LoggingTarget.Database); + using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. { try @@ -280,7 +300,7 @@ namespace osu.Game.Database var notification = new ProgressNotification { Progress = 0, - CompletionText = "Deleted all beatmaps!", + CompletionText = $"Deleted all {typeof(TModel).Name.Replace("Info", "").ToLower()}s!", State = ProgressNotificationState.Active, }; @@ -382,12 +402,47 @@ namespace osu.Game.Database return fileInfos; } + #region osu-stable import + + /// + /// Set a storage with access to an osu-stable install for import purposes. + /// + public Func GetStableStorage { private get; set; } + + /// + /// Denotes whether an osu-stable installation is present to perform automated imports from. + /// + public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; + + /// + /// The relative path from osu-stable's data directory to import items from. + /// + protected virtual string ImportFromStablePath => null; + + /// + /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. + /// + public Task ImportFromStableAsync() + { + var stable = GetStableStorage?.Invoke(); + + if (stable == null) + { + Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); + return Task.CompletedTask; + } + + return Task.Factory.StartNew(() => Import(stable.GetDirectories(ImportFromStablePath).Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning); + } + + #endregion + /// /// Create a barebones model from the provided archive. /// Actual expensive population should be done in ; this should just prepare for duplicate checking. /// /// The archive to create the model for. - /// A model populated with minimal information. + /// A model populated with minimal information. Returning a null will abort importing silently. protected abstract TModel CreateModel(ArchiveReader archive); /// @@ -412,7 +467,7 @@ namespace osu.Game.Database private ArchiveReader getReaderFrom(string path) { if (ZipUtils.IsZipArchive(path)) - return new ZipArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path)); + return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), Path.GetFileName(path)); if (Directory.Exists(path)) return new LegacyFilesystemReader(path); throw new InvalidFormatException($"{path} is not a valid archive"); diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index e70d753114..2037612a09 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -5,7 +5,6 @@ using System; using System.Linq; using System.Threading; using Microsoft.EntityFrameworkCore.Storage; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Platform; namespace osu.Game.Database @@ -118,7 +117,9 @@ namespace osu.Game.Database private void recycleThreadContexts() { - threadContexts?.Values.ForEach(c => c.Dispose()); + // Contexts for other threads are not disposed as they may be in use elsewhere. Instead, fresh contexts are exposed + // for other threads to use, and we rely on the finalizer inside OsuDbContext to handle their previous contexts + threadContexts?.Value.Dispose(); threadContexts = new ThreadLocal(CreateContext, true); } diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index bf57644caf..20e144c033 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -75,6 +75,13 @@ namespace osu.Game.Database } } + ~OsuDbContext() + { + // DbContext does not contain a finalizer (https://github.com/aspnet/EntityFrameworkCore/issues/8872) + // This is used to clean up previous contexts when fresh contexts are exposed via DatabaseContextFactory + Dispose(); + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); diff --git a/osu.Game/Graphics/Backgrounds/Background.cs b/osu.Game/Graphics/Backgrounds/Background.cs index d5825a8c42..6fc8b0e070 100644 --- a/osu.Game/Graphics/Backgrounds/Background.cs +++ b/osu.Game/Graphics/Backgrounds/Background.cs @@ -5,8 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using OpenTK.Graphics; -using osu.Game.Graphics.Textures; namespace osu.Game.Graphics.Backgrounds { diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 01b09c0a40..0f382900ce 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -116,7 +116,7 @@ namespace osu.Game.Graphics.Backgrounds float adjustedAlpha = HideAlphaDiscrepancies ? // Cubically scale alpha to make it drop off more sharply. - (float)Math.Pow(DrawInfo.Colour.AverageColour.Linear.A, 3) : + (float)Math.Pow(DrawColourInfo.Colour.AverageColour.Linear.A, 3) : 1; float elapsedSeconds = (float)Time.Elapsed / 1000; @@ -235,7 +235,7 @@ namespace osu.Game.Graphics.Backgrounds Vector2Extensions.Transform(particle.Position * Size + new Vector2(-offset.X, offset.Y), DrawInfo.Matrix) ); - ColourInfo colourInfo = DrawInfo.Colour; + ColourInfo colourInfo = DrawColourInfo.Colour; colourInfo.ApplyChild(particle.Colour); Texture.DrawTriangle( diff --git a/osu.Game/Graphics/Containers/ShakeContainer.cs b/osu.Game/Graphics/Containers/ShakeContainer.cs new file mode 100644 index 0000000000..fde4d59f46 --- /dev/null +++ b/osu.Game/Graphics/Containers/ShakeContainer.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Graphics.Containers +{ + /// + /// A container that adds the ability to shake its contents. + /// + public class ShakeContainer : Container + { + /// + /// Shake the contents of this container. + /// + /// The maximum length the shake should last. + public void Shake(double maximumLength) + { + const float shake_amount = 8; + const float shake_duration = 30; + + // if we don't have enough time, don't bother shaking. + if (maximumLength < shake_duration * 2) + return; + + var sequence = this.MoveToX(shake_amount, shake_duration / 2, Easing.OutSine).Then() + .MoveToX(-shake_amount, shake_duration, Easing.InOutSine).Then(); + + // if we don't have enough time for the second shake, skip it. + if (maximumLength > shake_duration * 4) + sequence = sequence + .MoveToX(shake_amount, shake_duration, Easing.InOutSine).Then() + .MoveToX(-shake_amount, shake_duration, Easing.InOutSine).Then(); + + sequence.MoveToX(0, shake_duration / 2, Easing.InSine); + } + } +} diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 406fc2ffd2..be794d93a6 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -4,7 +4,6 @@ using System; using Humanizer; using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Sprites; @@ -16,7 +15,6 @@ namespace osu.Game.Graphics public DrawableDate(DateTimeOffset date) { - AutoSizeAxes = Axes.Both; Font = "Exo2.0-RegularItalic"; Date = date.ToLocalTime(); diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 7b3337cb23..be253f65c1 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Drawing.Imaging; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -19,6 +18,7 @@ using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using SixLabors.ImageSharp; namespace osu.Game.Graphics { @@ -71,7 +71,7 @@ namespace osu.Game.Graphics private volatile int screenShotTasks; - public async Task TakeScreenshotAsync() => await Task.Run(async () => + public Task TakeScreenshotAsync() => Task.Run(async () => { Interlocked.Increment(ref screenShotTasks); @@ -90,7 +90,7 @@ namespace osu.Game.Graphics waitDelegate.Cancel(); } - using (var bitmap = await host.TakeScreenshotAsync()) + using (var image = await host.TakeScreenshotAsync()) { Interlocked.Decrement(ref screenShotTasks); @@ -102,10 +102,10 @@ namespace osu.Game.Graphics switch (screenshotFormat.Value) { case ScreenshotFormat.Png: - bitmap.Save(stream, ImageFormat.Png); + image.SaveAsPng(stream); break; case ScreenshotFormat.Jpg: - bitmap.Save(stream, ImageFormat.Jpeg); + image.SaveAsJpeg(stream); break; default: throw new ArgumentOutOfRangeException(nameof(screenshotFormat)); diff --git a/osu.Game/Graphics/SpriteIcon.cs b/osu.Game/Graphics/SpriteIcon.cs index 6acd20719e..1b1df45c77 100644 --- a/osu.Game/Graphics/SpriteIcon.cs +++ b/osu.Game/Graphics/SpriteIcon.cs @@ -71,7 +71,7 @@ namespace osu.Game.Graphics if (loadableIcon == loadedIcon) return; - var texture = store?.Get(((char)loadableIcon).ToString()); + var texture = store.Get(((char)loadableIcon).ToString()); spriteMain.Texture = texture; spriteShadow.Texture = texture; @@ -95,7 +95,7 @@ namespace osu.Game.Graphics { //adjust shadow alpha based on highest component intensity to avoid muddy display of darker text. //squared result for quadratic fall-off seems to give the best result. - var avgColour = (Color4)DrawInfo.Colour.AverageColour; + var avgColour = (Color4)DrawColourInfo.Colour.AverageColour; spriteShadow.Alpha = (float)Math.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2); @@ -129,7 +129,7 @@ namespace osu.Game.Graphics if (icon == value) return; icon = value; - if (IsLoaded) + if (LoadState == LoadState.Loaded) updateTexture(); } } diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs index c9389bb9e2..8607d51e12 100644 --- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs +++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs @@ -3,9 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Framework.MathUtils; -using OpenTK; -using OpenTK.Graphics; using osu.Framework.Graphics.Transforms; namespace osu.Game.Graphics.Sprites @@ -19,27 +16,6 @@ namespace osu.Game.Graphics.Sprites Shadow = true; TextSize = FONT_SIZE; } - - protected override Drawable CreateFallbackCharacterDrawable() - { - var tex = GetTextureForCharacter('?'); - - if (tex != null) - { - float adjust = (RNG.NextSingle() - 0.5f) * 2; - return new Sprite - { - Texture = tex, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Scale = new Vector2(1 + adjust * 0.2f), - Rotation = adjust * 15, - Colour = Color4.White, - }; - } - - return base.CreateFallbackCharacterDrawable(); - } } public static class OsuSpriteTextTransformExtensions diff --git a/osu.Game/Graphics/Textures/LargeTextureStore.cs b/osu.Game/Graphics/Textures/LargeTextureStore.cs deleted file mode 100644 index 4dcbb1220d..0000000000 --- a/osu.Game/Graphics/Textures/LargeTextureStore.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; - -namespace osu.Game.Graphics.Textures -{ - /// - /// A texture store that bypasses atlasing. - /// - public class LargeTextureStore : TextureStore - { - public LargeTextureStore(IResourceStore store = null) : base(store, false) - { - } - } -} diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index bf3805a44d..bb6a032a12 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -1,7 +1,16 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.EventArgs; +using osu.Framework.Input.States; +using osu.Game.Graphics.Sprites; +using OpenTK.Graphics; namespace osu.Game.Graphics.UserInterface { @@ -10,9 +19,73 @@ namespace osu.Game.Graphics.UserInterface /// public class OsuButton : Button { + private Box hover; + public OsuButton() { - Add(new HoverClickSounds(HoverSampleSet.Loud)); + Height = 40; + + Content.Masking = true; + Content.CornerRadius = 5; } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.BlueDark; + + AddRange(new Drawable[] + { + hover = new Box + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingMode.Additive, + Colour = Color4.White.Opacity(0.1f), + Alpha = 0, + Depth = -1 + }, + new HoverClickSounds(HoverSampleSet.Loud), + }); + + Enabled.ValueChanged += enabled_ValueChanged; + Enabled.TriggerChange(); + } + + private void enabled_ValueChanged(bool enabled) + { + this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint); + } + + protected override bool OnHover(InputState state) + { + hover.FadeIn(200); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + hover.FadeOut(200); + base.OnHoverLost(state); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + Content.ScaleTo(0.9f, 4000, Easing.OutQuint); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + Content.ScaleTo(1, 1000, Easing.OutElastic); + return base.OnMouseUp(state, args); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Font = @"Exo2.0-Bold", + }; } } diff --git a/osu.Game/Graphics/UserInterface/TriangleButton.cs b/osu.Game/Graphics/UserInterface/TriangleButton.cs index bfdc0c3bef..683b442d93 100644 --- a/osu.Game/Graphics/UserInterface/TriangleButton.cs +++ b/osu.Game/Graphics/UserInterface/TriangleButton.cs @@ -2,17 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using OpenTK.Graphics; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.EventArgs; -using osu.Framework.Input.States; using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface { @@ -21,79 +14,17 @@ namespace osu.Game.Graphics.UserInterface /// public class TriangleButton : OsuButton, IFilterable { - private Box hover; - - protected Triangles Triangles; - - public TriangleButton() - { - Height = 40; - } - - protected override SpriteText CreateText() => new OsuSpriteText - { - Depth = -1, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Font = @"Exo2.0-Bold", - }; + protected Triangles Triangles { get; private set; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = colours.BlueDark; - - Content.Masking = true; - Content.CornerRadius = 5; - - AddRange(new Drawable[] + Add(Triangles = new Triangles { - Triangles = new Triangles - { - RelativeSizeAxes = Axes.Both, - ColourDark = colours.BlueDarker, - ColourLight = colours.Blue, - }, - hover = new Box - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Colour = Color4.White.Opacity(0.1f), - Alpha = 0, - }, + RelativeSizeAxes = Axes.Both, + ColourDark = colours.BlueDarker, + ColourLight = colours.Blue, }); - - Enabled.ValueChanged += enabled_ValueChanged; - Enabled.TriggerChange(); - } - - private void enabled_ValueChanged(bool enabled) - { - this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint); - } - - protected override bool OnHover(InputState state) - { - hover.FadeIn(200); - return base.OnHover(state); - } - - protected override void OnHoverLost(InputState state) - { - hover.FadeOut(200); - base.OnHoverLost(state); - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - Content.ScaleTo(0.9f, 4000, Easing.OutQuint); - return base.OnMouseDown(state, args); - } - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) - { - Content.ScaleTo(1, 1000, Easing.OutElastic); - return base.OnMouseUp(state, args); } public IEnumerable FilterTerms => new[] { Text }; diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs index 808ce159bb..24a5094586 100644 --- a/osu.Game/IO/Archives/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using osu.Framework.IO.Stores; namespace osu.Game.IO.Archives @@ -28,7 +29,9 @@ namespace osu.Game.IO.Archives public abstract IEnumerable Filenames { get; } - public virtual byte[] Get(string name) + public virtual byte[] Get(string name) => GetAsync(name).Result; + + public async Task GetAsync(string name) { using (Stream input = GetStream(name)) { @@ -36,7 +39,7 @@ namespace osu.Game.IO.Archives return null; byte[] buffer = new byte[input.Length]; - input.Read(buffer, 0, buffer.Length); + await input.ReadAsync(buffer, 0, buffer.Length); return buffer; } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 12935a5ffe..1dda257c95 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -2,12 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Logging; @@ -26,7 +24,7 @@ namespace osu.Game.Online.API private const string client_id = @"5"; private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; - private ConcurrentQueue queue = new ConcurrentQueue(); + private readonly Queue queue = new Queue(); /// /// The username/email provided by the user when initiating a login. @@ -55,7 +53,13 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; - Task.Factory.StartNew(run, cancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + var thread = new Thread(run) + { + Name = "APIAccess", + IsBackground = true + }; + + thread.Start(); } private void onTokenChanged(OAuthToken token) => config.Set(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty); @@ -75,10 +79,7 @@ namespace osu.Game.Online.API public void Unregister(IOnlineComponent component) { - Scheduler.Add(delegate - { - components.Remove(component); - }); + Scheduler.Add(delegate { components.Remove(component); }); } public string AccessToken => authentication.RequestAccessToken(); @@ -103,6 +104,7 @@ namespace osu.Game.Online.API log.Add(@"Queueing a ping request"); Queue(new ListChannelsRequest { Timeout = 5000 }); } + break; case APIState.Offline: case APIState.Connecting: @@ -161,18 +163,21 @@ namespace osu.Game.Online.API continue; } - //process the request queue. - APIRequest req; - while (queue.TryPeek(out req)) + while (true) { - if (handleRequest(req)) + APIRequest req; + + lock (queue) { - //we have succeeded, so let's unqueue. - queue.TryDequeue(out req); + if (queue.Count == 0) break; + req = queue.Dequeue(); } + + // TODO: handle failures better + handleRequest(req); } - Thread.Sleep(1); + Thread.Sleep(50); } } @@ -205,7 +210,8 @@ namespace osu.Game.Online.API } catch (WebException we) { - HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout); + HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode + ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout); // special cases for un-typed but useful message responses. switch (we.Message) @@ -247,6 +253,7 @@ namespace osu.Game.Online.API } private APIState state; + public APIState State { get { return state; } @@ -271,7 +278,10 @@ namespace osu.Game.Online.API public bool IsLoggedIn => LocalUser.Value.Id > 1; - public void Queue(APIRequest request) => queue.Enqueue(request); + public void Queue(APIRequest request) + { + lock (queue) queue.Enqueue(request); + } public event StateChangeDelegate OnStateChange; @@ -279,16 +289,17 @@ namespace osu.Game.Online.API private void flushQueue(bool failOldRequests = true) { - var oldQueue = queue; - - //flush the queue. - queue = new ConcurrentQueue(); - - if (failOldRequests) + lock (queue) { - APIRequest req; - while (oldQueue.TryDequeue(out req)) - req.Fail(new WebException(@"Disconnected from server")); + var oldQueueRequests = queue.ToArray(); + + queue.Clear(); + + if (failOldRequests) + { + foreach (var req in oldQueueRequests) + req.Fail(new WebException(@"Disconnected from server")); + } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 025d5f50e3..6fcb948298 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -36,6 +36,8 @@ using osu.Game.Skinning; using OpenTK.Graphics; using osu.Game.Overlays.Volume; using osu.Game.Screens.Select; +using osu.Game.Utils; +using LogLevel = osu.Framework.Logging.LogLevel; namespace osu.Game { @@ -65,16 +67,18 @@ namespace osu.Game private ScreenshotManager screenshotManager; + protected RavenLogger RavenLogger; + public virtual Storage GetStorageForStableInstall() => null; private Intro intro { get { - Screen s = screenStack; - while (s != null && !(s is Intro)) - s = s.ChildScreen; - return s as Intro; + Screen screen = screenStack; + while (screen != null && !(screen is Intro)) + screen = screen.ChildScreen; + return screen as Intro; } } @@ -99,13 +103,17 @@ namespace osu.Game private readonly List overlays = new List(); // todo: move this to SongSelect once Screen has the ability to unsuspend. - public readonly Bindable> SelectedMods = new Bindable>(new List()); + [Cached] + [Cached(Type = typeof(IBindable>))] + private readonly Bindable> selectedMods = new Bindable>(new Mod[] { }); public OsuGame(string[] args = null) { this.args = args; forwardLoggedErrorsToNotifications(); + + RavenLogger = new RavenLogger(this); } public void ToggleSettings() => settings.ToggleVisibility(); @@ -118,8 +126,8 @@ namespace osu.Game /// Whether the toolbar should also be hidden. public void CloseAllOverlays(bool toolbar = true) { - foreach (var o in overlays) - o.State = Visibility.Hidden; + foreach (var overlay in overlays) + overlay.State = Visibility.Hidden; if (toolbar) Toolbar.State = Visibility.Hidden; } @@ -143,13 +151,15 @@ namespace osu.Game if (args?.Length > 0) { - var paths = args.Where(a => !a.StartsWith(@"-")); - - Task.Run(() => Import(paths.ToArray())); + var paths = args.Where(a => !a.StartsWith(@"-")).ToArray(); + if (paths.Length > 0) + Task.Run(() => Import(paths)); } dependencies.CacheAs(this); + dependencies.Cache(RavenLogger); + dependencies.CacheAs(ruleset); dependencies.CacheAs>(ruleset); @@ -234,7 +244,7 @@ namespace osu.Game /// The beatmap to show. public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId); - protected void LoadScore(Score s) + protected void LoadScore(Score score) { scoreLoad?.Cancel(); @@ -242,18 +252,18 @@ namespace osu.Game if (menu == null) { - scoreLoad = Schedule(() => LoadScore(s)); + scoreLoad = Schedule(() => LoadScore(score)); return; } if (!menu.IsCurrentScreen) { menu.MakeCurrent(); - this.Delay(500).Schedule(() => LoadScore(s), out scoreLoad); + this.Delay(500).Schedule(() => LoadScore(score), out scoreLoad); return; } - if (s.Beatmap == null) + if (score.Beatmap == null) { notifications.Post(new SimpleNotification { @@ -263,12 +273,18 @@ namespace osu.Game return; } - ruleset.Value = s.Ruleset; + ruleset.Value = score.Ruleset; - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(s.Beatmap); - Beatmap.Value.Mods.Value = s.Mods; + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(score.Beatmap); + Beatmap.Value.Mods.Value = score.Mods; - menu.Push(new PlayerLoader(new ReplayPlayer(s.Replay))); + menu.Push(new PlayerLoader(new ReplayPlayer(score.Replay))); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + RavenLogger.Dispose(); } protected override void LoadComplete() @@ -283,11 +299,13 @@ namespace osu.Game // This prevents the cursor from showing until we have a screen with CursorVisible = true MenuCursorContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; - // hook up notifications to components. + // todo: all archive managers should be able to be looped here. SkinManager.PostNotification = n => notifications?.Post(n); - BeatmapManager.PostNotification = n => notifications?.Post(n); + SkinManager.GetStableStorage = GetStorageForStableInstall; + BeatmapManager.PostNotification = n => notifications?.Post(n); BeatmapManager.GetStableStorage = GetStorageForStableInstall; + BeatmapManager.PresentBeatmap = PresentBeatmap; AddRange(new Drawable[] @@ -447,7 +465,7 @@ namespace osu.Game Schedule(() => notifications.Post(new SimpleNotification { Icon = entry.Level == LogLevel.Important ? FontAwesome.fa_exclamation_circle : FontAwesome.fa_bomb, - Text = entry.Message, + Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), })); } else if (recentLogCount == short_term_display_limit) @@ -488,7 +506,27 @@ namespace osu.Game // schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached). // with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile, // we could avoid the need for scheduling altogether. - Schedule(() => { asyncLoadStream = asyncLoadStream?.ContinueWith(t => LoadComponentAsync(d, add).Wait()) ?? LoadComponentAsync(d, add); }); + Schedule(() => + { + var previousLoadStream = asyncLoadStream; + + //chain with existing load stream + asyncLoadStream = Task.Run(async () => + { + if (previousLoadStream != null) + await previousLoadStream; + + try + { + Logger.Log($"Loading {d}...", LoggingTarget.Debug); + await LoadComponentAsync(d, add); + Logger.Log($"Loaded {d}!", LoggingTarget.Debug); + } + catch (OperationCanceledException) + { + } + }); + }); } public bool OnPressed(GlobalAction action) @@ -599,6 +637,7 @@ namespace osu.Game private void screenAdded(Screen newScreen) { currentScreen = (OsuScreen)newScreen; + Logger.Log($"Screen changed → {currentScreen}"); newScreen.ModePushed += screenAdded; newScreen.Exited += screenRemoved; @@ -607,6 +646,7 @@ namespace osu.Game private void screenRemoved(Screen newScreen) { currentScreen = (OsuScreen)newScreen; + Logger.Log($"Screen changed ← {currentScreen}"); if (newScreen == null) Exit(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index bada2a794d..9a5dac35b9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -24,7 +24,6 @@ using osu.Framework.Input; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Database; -using osu.Game.Graphics.Textures; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; @@ -109,11 +108,39 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Host.Storage)); - dependencies.Cache(new LargeTextureStore(new RawTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")))); + var largeStore = new LargeTextureStore(new TextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); + largeStore.AddStore(new TextureLoaderStore(new OnlineStore())); + dependencies.Cache(largeStore); dependencies.CacheAs(this); dependencies.Cache(LocalConfig); + //this completely overrides the framework default. will need to change once we make a proper FontStore. + dependencies.Cache(Fonts = new FontStore(new GlyphStore(Resources, @"Fonts/FontAwesome"))); + + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/osuFont")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Medium")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-MediumItalic")); + + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-Basic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-Hangul")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-CJK-Basic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-CJK-Compatibility")); + + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Regular")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-RegularItalic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-SemiBold")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-SemiBoldItalic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Bold")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-BoldItalic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Light")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-LightItalic")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Black")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-BlackItalic")); + + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera")); + Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera-Light")); + runMigrations(); dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio)); @@ -137,33 +164,6 @@ namespace osu.Game fileImporters.Add(ScoreStore); fileImporters.Add(SkinManager); - //this completely overrides the framework default. will need to change once we make a proper FontStore. - dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 }); - - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/FontAwesome")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/osuFont")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Medium")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-MediumItalic")); - - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-Basic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-Hangul")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-CJK-Basic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Noto-CJK-Compatibility")); - - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Regular")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-RegularItalic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-SemiBold")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-SemiBoldItalic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Bold")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-BoldItalic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Light")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-LightItalic")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-Black")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Exo2.0-BlackItalic")); - - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera")); - Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera-Light")); - var defaultBeatmap = new DummyWorkingBeatmap(this); beatmap = new OsuBindableBeatmap(defaultBeatmap, Audio); BeatmapManager.DefaultBeatmap = defaultBeatmap; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 626de14c98..60811d8b12 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores loading = true; getScoresRequest = new GetScoresRequest(beatmap, beatmap.Ruleset); - getScoresRequest.Success += r => Scores = r.Scores; + getScoresRequest.Success += r => Schedule(() => Scores = r.Scores); api.Queue(getScoresRequest); } } @@ -134,5 +134,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores this.api = api; updateDisplay(); } + + protected override void Dispose(bool isDisposing) + { + getScoresRequest?.Cancel(); + } } } diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index 45e1164a57..850ead37f6 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.States; using osu.Game.Beatmaps; namespace osu.Game.Overlays.Direct @@ -26,12 +25,14 @@ namespace osu.Game.Overlays.Direct private PlayButton playButton; private Box progressBar; - private Container downloadContainer; + + protected override bool FadePlayButton => false; protected override PlayButton PlayButton => playButton; protected override Box PreviewBar => progressBar; - public DirectListPanel(BeatmapSetInfo beatmap) : base(beatmap) + public DirectListPanel(BeatmapSetInfo beatmap) + : base(beatmap) { RelativeSizeAxes = Axes.X; Height = height; @@ -66,30 +67,45 @@ namespace osu.Game.Overlays.Direct Spacing = new Vector2(10, 0), Children = new Drawable[] { - playButton = new PlayButton(SetInfo) - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Size = new Vector2(height / 2), - FillMode = FillMode.Fit, - Alpha = 0, - }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + new FillFlowContainer { - Current = localisation.GetUnicodePreference(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), - TextSize = 18, - Font = @"Exo2.0-BoldItalic", - }, - new OsuSpriteText - { - Current = localisation.GetUnicodePreference(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), - Font = @"Exo2.0-BoldItalic", + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + playButton = new PlayButton(SetInfo) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Size = new Vector2(height / 2), + FillMode = FillMode.Fit, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Current = localisation.GetUnicodePreference(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), + TextSize = 18, + Font = @"Exo2.0-BoldItalic", + }, + new OsuSpriteText + { + Current = localisation.GetUnicodePreference(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), + Font = @"Exo2.0-BoldItalic", + }, + } + }, + } }, new FillFlowContainer { @@ -108,16 +124,13 @@ namespace osu.Game.Overlays.Direct Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - LayoutEasing = Easing.OutQuint, - LayoutDuration = transition_duration, Children = new Drawable[] { - downloadContainer = new Container + new Container { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, - Alpha = 0, Child = new DownloadButton(SetInfo) { Size = new Vector2(height - vertical_padding * 3), @@ -184,17 +197,5 @@ namespace osu.Game.Overlays.Direct }, }); } - - protected override bool OnHover(InputState state) - { - downloadContainer.FadeIn(transition_duration, Easing.InOutQuint); - return base.OnHover(state); - } - - protected override void OnHoverLost(InputState state) - { - downloadContainer.FadeOut(transition_duration, Easing.InOutQuint); - base.OnHoverLost(state); - } } } diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index 7d5c0c16cc..2ee1857ca2 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -40,6 +40,8 @@ namespace osu.Game.Overlays.Direct protected abstract PlayButton PlayButton { get; } protected abstract Box PreviewBar { get; } + protected virtual bool FadePlayButton => true; + protected override Container Content => content; protected DirectPanel(BeatmapSetInfo setInfo) @@ -102,8 +104,6 @@ namespace osu.Game.Overlays.Direct beatmaps.ItemAdded += setAdded; } - public override bool DisposeOnDeathRemoval => true; - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -125,7 +125,8 @@ namespace osu.Game.Overlays.Direct { content.TweenEdgeEffectTo(edgeEffectHovered, hover_transition_time, Easing.OutQuint); content.MoveToY(-4, hover_transition_time, Easing.OutQuint); - PlayButton.FadeIn(120, Easing.InOutQuint); + if (FadePlayButton) + PlayButton.FadeIn(120, Easing.InOutQuint); return base.OnHover(state); } @@ -134,7 +135,7 @@ namespace osu.Game.Overlays.Direct { content.TweenEdgeEffectTo(edgeEffectNormal, hover_transition_time, Easing.OutQuint); content.MoveToY(0, hover_transition_time, Easing.OutQuint); - if (!PreviewPlaying) + if (FadePlayButton && !PreviewPlaying) PlayButton.FadeOut(120, Easing.InOutQuint); base.OnHoverLost(state); @@ -184,7 +185,7 @@ namespace osu.Game.Overlays.Direct base.LoadComplete(); this.FadeInFromZero(200, Easing.Out); - PreviewPlaying.ValueChanged += newValue => PlayButton.FadeTo(newValue || IsHovered ? 1 : 0, 120, Easing.InOutQuint); + PreviewPlaying.ValueChanged += newValue => PlayButton.FadeTo(newValue || IsHovered || !FadePlayButton ? 1 : 0, 120, Easing.InOutQuint); PreviewPlaying.ValueChanged += newValue => PreviewBar.FadeTo(newValue ? 1 : 0, 120, Easing.InOutQuint); } diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 423211659d..f63d314053 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -240,6 +240,15 @@ namespace osu.Game.Overlays }); } + protected override void PopIn() + { + base.PopIn(); + + // Queries are allowed to be run only on the first pop-in + if (getSetsRequest == null) + Scheduler.AddOnce(updateSearch); + } + private SearchBeatmapSetsRequest getSetsRequest; private readonly Bindable currentQuery = new Bindable(); @@ -251,16 +260,22 @@ namespace osu.Game.Overlays { queryChangedDebounce?.Cancel(); - if (!IsLoaded) return; + if (!IsLoaded) + return; + + if (State == Visibility.Hidden) + return; BeatmapSets = null; ResultAmounts = null; getSetsRequest?.Cancel(); - if (api == null) return; + if (api == null) + return; - if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty)) return; + if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty)) + return; previewTrackManager.StopAnyPlaying(this); diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index a27278e002..629b6d6fa4 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -118,9 +118,9 @@ namespace osu.Game.Overlays.MedalSplash } [BackgroundDependencyLoader] - private void load(OsuColour colours, TextureStore textures) + private void load(OsuColour colours, TextureStore textures, LargeTextureStore largeTextures) { - medalSprite.Texture = textures.Get(medal.ImageUrl); + medalSprite.Texture = largeTextures.Get(medal.ImageUrl); medalGlow.Texture = textures.Get(@"MedalSplash/medal-glow"); description.Colour = colours.BlueLight; } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 4745eba68d..e83dedaf35 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -27,6 +27,11 @@ namespace osu.Game.Overlays.Mods { public class ModSelectOverlay : WaveOverlayContainer { + /// + /// How much this container should overflow the sides of the screen to account for parallax shifting. + /// + private const float overflow_padding = 50; + private const float content_width = 0.8f; protected Color4 LowMultiplierColour, HighMultiplierColour; @@ -39,9 +44,39 @@ namespace osu.Game.Overlays.Mods protected readonly FillFlowContainer ModSectionsContainer; - public readonly Bindable> SelectedMods = new Bindable>(); + protected readonly Bindable> SelectedMods = new Bindable>(new Mod[] { }); - public readonly IBindable Ruleset = new Bindable(); + protected readonly IBindable Ruleset = new Bindable(); + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, IBindable ruleset, AudioManager audio, Bindable> selectedMods) + { + LowMultiplierColour = colours.Red; + HighMultiplierColour = colours.Green; + UnrankedLabel.Colour = colours.Blue; + + Ruleset.BindTo(ruleset); + if (selectedMods != null) SelectedMods.BindTo(selectedMods); + + sampleOn = audio.Sample.Get(@"UI/check-on"); + sampleOff = audio.Sample.Get(@"UI/check-off"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Ruleset.BindValueChanged(rulesetChanged, true); + SelectedMods.BindValueChanged(selectedModsChanged, true); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + Ruleset.UnbindAll(); + SelectedMods.UnbindAll(); + } private void rulesetChanged(RulesetInfo newRuleset) { @@ -51,33 +86,16 @@ namespace osu.Game.Overlays.Mods foreach (ModSection section in ModSectionsContainer.Children) section.Mods = instance.GetModsFor(section.ModType); + + // attempt to re-select any already selected mods. + // this may be the first time we are receiving the ruleset, in which case they will still match. + selectedModsChanged(SelectedMods.Value); + + // write the mods back to the SelectedMods bindable in the case a change was not applicable. + // this generally isn't required as the previous line will perform deselection; just here for safety. refreshSelectedMods(); } - [BackgroundDependencyLoader] - private void load(OsuColour colours, IBindable ruleset, AudioManager audio) - { - SelectedMods.ValueChanged += selectedModsChanged; - - LowMultiplierColour = colours.Red; - HighMultiplierColour = colours.Green; - UnrankedLabel.Colour = colours.Blue; - - Ruleset.BindTo(ruleset); - Ruleset.BindValueChanged(rulesetChanged, true); - - sampleOn = audio.Sample.Get(@"UI/check-on"); - sampleOff = audio.Sample.Get(@"UI/check-off"); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - Ruleset.UnbindAll(); - SelectedMods.UnbindAll(); - } - private void selectedModsChanged(IEnumerable obj) { foreach (ModSection section in ModSectionsContainer.Children) @@ -176,10 +194,7 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } - private void refreshSelectedMods() - { - SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); - } + private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); public ModSelectOverlay() { @@ -189,6 +204,11 @@ namespace osu.Game.Overlays.Mods Waves.FourthWaveColour = OsuColour.FromHex(@"003a4e"); Height = 510; + Padding = new MarginPadding + { + Left = -overflow_padding, + Right = -overflow_padding + }; Children = new Drawable[] { @@ -248,6 +268,11 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Width = content_width, + Padding = new MarginPadding + { + Left = overflow_padding, + Right = overflow_padding + }, Children = new Drawable[] { new OsuSpriteText @@ -285,7 +310,12 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Vertical = 10 }, + Padding = new MarginPadding + { + Vertical = 10, + Left = overflow_padding, + Right = overflow_padding + }, Child = ModSectionsContainer = new FillFlowContainer { Origin = Anchor.TopCentre, @@ -331,7 +361,9 @@ namespace osu.Game.Overlays.Mods Direction = FillDirection.Horizontal, Padding = new MarginPadding { - Vertical = 15 + Vertical = 15, + Left = overflow_padding, + Right = overflow_padding }, Children = new Drawable[] { diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index d891cd96e8..78f8f57343 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -96,8 +96,7 @@ namespace osu.Game.Overlays base.LoadComplete(); StateChanged += _ => updateProcessingMode(); - OverlayActivationMode.ValueChanged += _ => updateProcessingMode(); - OverlayActivationMode.TriggerChange(); + OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true); } private int totalCount => sections.Select(c => c.DisplayedCount).Sum(); diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 7a07fb970c..254258d098 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -169,7 +169,7 @@ namespace osu.Game.Overlays.Notifications public Action CompletionTarget { get; set; } /// - /// An action to complete when the completion notification is clicked. + /// An action to complete when the completion notification is clicked. Return true to close. /// public Func CompletionClickAction; diff --git a/osu.Game/Overlays/Profile/Header/BadgeContainer.cs b/osu.Game/Overlays/Profile/Header/BadgeContainer.cs index bfade5e45c..33baaf1fff 100644 --- a/osu.Game/Overlays/Profile/Header/BadgeContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BadgeContainer.cs @@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Profile.Header } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { Child = new Sprite { diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 0b06acd426..621f752b9c 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -14,8 +14,8 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps public class PaginatedBeatmapContainer : PaginatedContainer { private const float panel_padding = 10f; - private readonly BeatmapSetType type; + private GetUserBeatmapsRequest request; public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string header, string missing = "None... yet.") : base(user, header, missing) @@ -31,9 +31,8 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps { base.ShowMore(); - var req = new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); - - req.Success += sets => + request = new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); + request.Success += sets => Schedule(() => { ShowMoreButton.FadeTo(sets.Count == ItemsPerPage ? 1 : 0); ShowMoreLoading.Hide(); @@ -52,9 +51,15 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps var panel = new DirectGridPanel(s.ToBeatmapSet(Rulesets)); ItemsContainer.Add(panel); } - }; + }); - Api.Queue(req); + Api.Queue(request); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + request?.Cancel(); } } } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 42784682be..ad886c363b 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -12,6 +12,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { + private GetUserMostPlayedBeatmapsRequest request; + public PaginatedMostPlayedBeatmapContainer(Bindable user) :base(user, "Most Played Beatmaps", "No records. :(") { @@ -24,9 +26,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { base.ShowMore(); - var req = new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); - - req.Success += beatmaps => + request = new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); + request.Success += beatmaps => Schedule(() => { ShowMoreButton.FadeTo(beatmaps.Count == ItemsPerPage ? 1 : 0); ShowMoreLoading.Hide(); @@ -43,9 +44,16 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { ItemsContainer.Add(new DrawableMostPlayedRow(beatmap.GetBeatmapInfo(Rulesets), beatmap.PlayCount)); } - }; + }); - Api.Queue(req); + Api.Queue(request); + } + + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + request?.Cancel(); } } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 6dbb9b9ba3..db93fcbc1b 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Profile.Sections protected readonly Bindable User = new Bindable(); protected APIAccess Api; + protected APIRequest RetrievalRequest; protected RulesetStore Rulesets; public PaginatedContainer(Bindable user, string header, string missing) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 707de0a10f..ed82c62e5c 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -16,6 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { private readonly bool includeWeight; private readonly ScoreType type; + private GetUserScoresRequest request; public PaginatedScoreContainer(ScoreType type, Bindable user, string header, string missing, bool includeWeight = false) : base(user, header, missing) @@ -32,9 +33,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { base.ShowMore(); - var req = new GetUserScoresRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); - - req.Success += scores => + request = new GetUserScoresRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); + request.Success += scores => Schedule(() => { foreach (var s in scores) s.ApplyRuleset(Rulesets.GetRuleset(s.OnlineRulesetID)); @@ -66,9 +66,15 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks ItemsContainer.Add(drawableScore); } - }; + }); - Api.Queue(req); + Api.Queue(request); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + request?.Cancel(); } } } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs index 0d354c728f..45569271df 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { sprite.Texture = textures.Get(url); } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index ee2f2f5973..fd5eda4e44 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -12,6 +12,8 @@ namespace osu.Game.Overlays.Profile.Sections.Recent { public class PaginatedRecentActivityContainer : PaginatedContainer { + private GetUserRecentActivitiesRequest request; + public PaginatedRecentActivityContainer(Bindable user, string header, string missing) : base(user, header, missing) { @@ -22,9 +24,8 @@ namespace osu.Game.Overlays.Profile.Sections.Recent { base.ShowMore(); - var req = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); - - req.Success += activities => + request = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); + request.Success += activities => Schedule(() => { ShowMoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0); ShowMoreLoading.Hide(); @@ -41,9 +42,15 @@ namespace osu.Game.Overlays.Profile.Sections.Recent { ItemsContainer.Add(new DrawableRecentActivity(activity)); } - }; + }); - Api.Queue(req); + Api.Queue(request); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + request?.Cancel(); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs index 9f550413f3..b14a4b8773 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs @@ -13,15 +13,18 @@ namespace osu.Game.Overlays.Settings.Sections.Debug { protected override string Header => "Garbage Collector"; + private readonly Bindable latencyMode = new Bindable(); + private Bindable configLatencyMode; + [BackgroundDependencyLoader] private void load(FrameworkDebugConfigManager config) { Children = new Drawable[] { - new SettingsEnumDropdown + new SettingsEnumDropdown { LabelText = "Active mode", - Bindable = config.GetBindable(DebugSetting.ActiveGCMode) + Bindable = latencyMode }, new SettingsButton { @@ -29,6 +32,18 @@ namespace osu.Game.Overlays.Settings.Sections.Debug Action = GC.Collect }, }; + + configLatencyMode = config.GetBindable(DebugSetting.ActiveGCMode); + configLatencyMode.BindValueChanged(v => latencyMode.Value = (LatencyMode)v, true); + latencyMode.BindValueChanged(v => configLatencyMode.Value = (GCLatencyMode)v); + } + + private enum LatencyMode + { + Batch = GCLatencyMode.Batch, + Interactive = GCLatencyMode.Interactive, + LowLatency = GCLatencyMode.LowLatency, + SustainedLowLatency = GCLatencyMode.SustainedLowLatency } } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 42028f6bd5..4a8164c6df 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -1,8 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; +using System.Drawing; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,21 +19,36 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private FillFlowContainer letterboxSettings; private Bindable letterboxing; + private Bindable sizeFullscreen; + + private OsuGameBase game; + private SettingsDropdown resolutionDropdown; + private SettingsEnumDropdown windowModeDropdown; private const int transition_duration = 400; [BackgroundDependencyLoader] - private void load(FrameworkConfigManager config) + private void load(FrameworkConfigManager config, OsuGameBase game) { + this.game = game; + letterboxing = config.GetBindable(FrameworkSetting.Letterboxing); + sizeFullscreen = config.GetBindable(FrameworkSetting.SizeFullscreen); Children = new Drawable[] { - new SettingsEnumDropdown + windowModeDropdown = new SettingsEnumDropdown { LabelText = "Screen mode", Bindable = config.GetBindable(FrameworkSetting.WindowMode), }, + resolutionDropdown = new SettingsDropdown + { + LabelText = "Resolution", + ShowsDefaultIndicator = false, + Items = getResolutions(), + Bindable = sizeFullscreen + }, new SettingsCheckbox { LabelText = "Letterboxing", @@ -62,15 +81,39 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, }; - letterboxing.ValueChanged += isVisible => + windowModeDropdown.Bindable.BindValueChanged(windowMode => + { + if (windowMode == WindowMode.Fullscreen) + { + resolutionDropdown.Show(); + sizeFullscreen.TriggerChange(); + } + else + resolutionDropdown.Hide(); + }, true); + + letterboxing.BindValueChanged(isVisible => { letterboxSettings.ClearTransforms(); letterboxSettings.AutoSizeAxes = isVisible ? Axes.Y : Axes.None; if (!isVisible) letterboxSettings.ResizeHeightTo(0, transition_duration, Easing.OutQuint); - }; - letterboxing.TriggerChange(); + }, true); + } + + private IEnumerable> getResolutions() + { + var resolutions = new KeyValuePair("Default", new Size(9999, 9999)).Yield(); + + if (game.Window != null) + resolutions = resolutions.Concat(game.Window.AvailableResolutions + .Where(r => r.Width >= 800 && r.Height >= 600) + .OrderByDescending(r => r.Width) + .ThenByDescending(r => r.Height) + .Select(res => new KeyValuePair($"{res.Width}x{res.Height}", new Size(res.Width, res.Height))) + .Distinct()).ToList(); + return resolutions; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 93d1986e3a..57e9a528d2 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Skinning; namespace osu.Game.Overlays.Settings.Sections.Maintenance { @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance protected override string Header => "General"; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, DialogOverlay dialogOverlay) + private void load(BeatmapManager beatmaps, SkinManager skins, DialogOverlay dialogOverlay) { Children = new Drawable[] { @@ -30,7 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { importButton.Enabled.Value = false; - beatmaps.ImportFromStable().ContinueWith(t => Schedule(() => importButton.Enabled.Value = true)); + beatmaps.ImportFromStableAsync().ContinueWith(t => Schedule(() => importButton.Enabled.Value = true)); } }, deleteButton = new DangerousSettingsButton @@ -45,6 +46,27 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance })); } }, + importButton = new SettingsButton + { + Text = "Import skins from stable", + Action = () => + { + importButton.Enabled.Value = false; + skins.ImportFromStableAsync().ContinueWith(t => Schedule(() => importButton.Enabled.Value = true)); + } + }, + deleteButton = new DangerousSettingsButton + { + Text = "Delete ALL skins", + Action = () => + { + dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => + { + deleteButton.Enabled.Value = false; + Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); + })); + } + }, restoreButton = new SettingsButton { Text = "Restore all hidden difficulties", diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index df8ebaf4aa..15787d29f7 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -51,10 +51,10 @@ namespace osu.Game.Overlays.Settings.Sections }, }; - skins.ItemAdded += onItemsChanged; - skins.ItemRemoved += onItemsChanged; + skins.ItemAdded += itemAdded; + skins.ItemRemoved += itemRemoved; - reloadSkins(); + skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); var skinBindable = config.GetBindable(OsuSetting.Skin); @@ -65,9 +65,8 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Bindable = skinBindable; } - private void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); - - private void onItemsChanged(SkinInfo _) => Schedule(reloadSkins); + private void itemRemoved(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Where(i => i.Value != s.ID); + private void itemAdded(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Append(new KeyValuePair(s.ToString(), s.ID)); protected override void Dispose(bool isDisposing) { @@ -75,8 +74,8 @@ namespace osu.Game.Overlays.Settings.Sections if (skins != null) { - skins.ItemAdded -= onItemsChanged; - skins.ItemRemoved -= onItemsChanged; + skins.ItemAdded -= itemAdded; + skins.ItemRemoved -= itemRemoved; } } diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 9555f0fbc5..58714208c0 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -73,7 +73,7 @@ namespace osu.Game.Overlays.Settings }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new[] + Children = new Drawable[] { new OsuSpriteText { diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index ac2c840c94..28e2b773ec 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.States; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -16,7 +17,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { - public class SidebarButton : OsuButton + public class SidebarButton : Button { private readonly SpriteIcon drawableIcon; private readonly SpriteText headerText; @@ -97,7 +98,8 @@ namespace osu.Game.Overlays.Settings Width = 5, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - } + }, + new HoverClickSounds(HoverSampleSet.Loud), }); } diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 2eabf1eb74..cdc3bc2b51 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -83,14 +83,14 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader(true)] private void load(OsuGame osuGame) { - if (osuGame != null) - overlayActivationMode.BindTo(osuGame.OverlayActivationMode); - StateChanged += visibility => { if (overlayActivationMode == OverlayActivation.Disabled) State = Visibility.Hidden; }; + + if (osuGame != null) + overlayActivationMode.BindTo(osuGame.OverlayActivationMode); } public class ToolbarBackground : Container diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 11b68b0e09..ea077ff645 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -73,16 +73,15 @@ namespace osu.Game.Overlays FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.Out); } - public void ShowUser(long userId) - { - if (userId == Header.User.Id) - return; - - ShowUser(new User { Id = userId }); - } + public void ShowUser(long userId) => ShowUser(new User { Id = userId }); public void ShowUser(User user, bool fetchOnline = true) { + Show(); + + if (user.Id == Header?.User.Id) + return; + userReq?.Cancel(); Clear(); lastSection = null; @@ -97,6 +96,7 @@ namespace osu.Game.Overlays new BeatmapsSection(), new KudosuSection() }; + tabs = new ProfileTabControl { RelativeSizeAxes = Axes.X, @@ -161,7 +161,6 @@ namespace osu.Game.Overlays userLoadComplete(user); } - Show(); sectionsContainer.ScrollToTop(); } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b2f6e909d2..a3253250f2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -23,12 +24,15 @@ namespace osu.Game.Rulesets.Edit { private readonly Ruleset ruleset; + public IEnumerable HitObjects => rulesetContainer.Playfield.AllHitObjects; + protected ICompositionTool CurrentTool { get; private set; } + protected IRulesetConfigManager Config { get; private set; } + + private readonly List layerContainers = new List(); + private readonly IBindable beatmap = new Bindable(); private RulesetContainer rulesetContainer; - private readonly List layerContainers = new List(); - - private readonly IBindable beatmap = new Bindable(); protected HitObjectComposer(Ruleset ruleset) { @@ -60,7 +64,7 @@ namespace osu.Game.Rulesets.Edit }; var layerAboveRuleset = CreateLayerContainer(); - layerAboveRuleset.Child = new HitObjectMaskLayer(rulesetContainer.Playfield, this); + layerAboveRuleset.Child = new HitObjectMaskLayer(); layerContainers.Add(layerBelowRuleset); layerContainers.Add(layerAboveRuleset); @@ -110,6 +114,16 @@ namespace osu.Game.Rulesets.Edit toolboxCollection.Items[0].Select(); } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.CacheAs(this); + Config = dependencies.Get().GetConfigFor(ruleset); + + return dependencies; + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs index 2c3720fc8f..78ad236e74 100644 --- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs @@ -8,6 +8,16 @@ namespace osu.Game.Rulesets.Edit.Tools public class HitObjectCompositionTool : ICompositionTool where T : HitObject { - public string Name => typeof(T).Name; + public string Name { get; } + + public HitObjectCompositionTool() + : this(typeof(T).Name) + { + } + + public HitObjectCompositionTool(string name) + { + Name = name; + } } } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 5de14ae579..65b2ef75c4 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Judgements private OsuColour colours; - protected readonly Judgement Judgement; + protected readonly JudgementResult Result; public readonly DrawableHitObject JudgedObject; @@ -34,11 +34,11 @@ namespace osu.Game.Rulesets.Judgements /// /// Creates a drawable which visualises a . /// - /// The judgement to visualise. + /// The judgement to visualise. /// The object which was judged. - public DrawableJudgement(Judgement judgement, DrawableHitObject judgedObject) + public DrawableJudgement(JudgementResult result, DrawableHitObject judgedObject) { - Judgement = judgement; + Result = result; JudgedObject = judgedObject; Size = new Vector2(judgement_size); @@ -49,11 +49,11 @@ namespace osu.Game.Rulesets.Judgements { this.colours = colours; - Child = new SkinnableDrawable($"Play/{Judgement.Result}", _ => JudgementText = new OsuSpriteText + Child = new SkinnableDrawable($"Play/{Result.Type}", _ => JudgementText = new OsuSpriteText { - Text = Judgement.Result.GetDescription().ToUpperInvariant(), + Text = Result.Type.GetDescription().ToUpperInvariant(), Font = @"Venera", - Colour = judgementColour(Judgement.Result), + Colour = judgementColour(Result.Type), Scale = new Vector2(0.85f, 1), TextSize = 12 }, restrictSize: false); @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Judgements this.FadeInFromZero(100, Easing.OutQuint); - switch (Judgement.Result) + switch (Result.Type) { case HitResult.None: break; diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 129dd07c3e..c679df5900 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -1,74 +1,48 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Judgements { + /// + /// The scoring information provided by a . + /// public class Judgement { - /// - /// Whether this judgement is the result of a hit or a miss. - /// - public HitResult Result; - /// /// The maximum that can be achieved. /// public virtual HitResult MaxResult => HitResult.Perfect; /// - /// The combo prior to this judgement occurring. - /// - public int ComboAtJudgement; - - /// - /// The highest combo achieved prior to this judgement occurring. - /// - public int HighestComboAtJudgement; - - /// - /// Whether a successful hit occurred. - /// - public bool IsHit => Result > HitResult.Miss; - - /// - /// Whether this judgement is the final judgement for the hit object. - /// - public bool Final = true; - - /// - /// The offset from a perfect hit at which this judgement occurred. - /// Populated when added via . - /// - public double TimeOffset { get; set; } - - /// - /// Whether the should affect the current combo. + /// Whether this should affect the current combo. /// public virtual bool AffectsCombo => true; /// - /// Whether the should be counted as base (combo) or bonus score. + /// Whether this should be counted as base (combo) or bonus score. /// public virtual bool IsBonus => !AffectsCombo; /// - /// The numeric representation for the result achieved. - /// - public int NumericResult => NumericResultFor(Result); - - /// - /// The numeric representation for the maximum achievable result. + /// The numeric score representation for the maximum achievable result. /// public int MaxNumericResult => NumericResultFor(MaxResult); /// - /// Convert a to a numeric score representation. + /// Retrieves the numeric score representation of a . /// - /// The value to convert. - /// The number. + /// The to find the numeric score representation for. + /// The numeric score representation of . protected virtual int NumericResultFor(HitResult result) => result > HitResult.Miss ? 1 : 0; + + /// + /// Retrieves the numeric score representation of a . + /// + /// The to find the numeric score representation for. + /// The numeric score representation of . + public int NumericResultFor(JudgementResult result) => NumericResultFor(result.Type); } } diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs new file mode 100644 index 0000000000..5cadf7e2ee --- /dev/null +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Judgements +{ + /// + /// The scoring result of a . + /// + public class JudgementResult + { + /// + /// Whether this is the result of a hit or a miss. + /// + public HitResult Type; + + /// + /// The which this applies for. + /// + public readonly Judgement Judgement; + + /// + /// The offset from a perfect hit at which this occurred. + /// Populated when this is applied via . + /// + public double TimeOffset { get; internal set; } + + /// + /// The combo prior to this occurring. + /// + public int ComboAtJudgement { get; internal set; } + + /// + /// The highest combo achieved prior to this occurring. + /// + public int HighestComboAtJudgement { get; internal set; } + + /// + /// Whether a miss or hit occurred. + /// + public bool HasResult => Type > HitResult.None; + + /// + /// Whether a successful hit occurred. + /// + public bool IsHit => Type > HitResult.Miss; + + /// + /// Creates a new . + /// + /// The to refer to for scoring information. + public JudgementResult(Judgement judgement) + { + Judgement = judgement; + } + } +} diff --git a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs new file mode 100644 index 0000000000..be879759bd --- /dev/null +++ b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mods +{ + public interface IUpdatableByPlayfield : IApplicableMod + { + void Update(Playfield playfield); + } +} diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index a22aaa784f..7e3e955740 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; +using osu.Framework.Extensions.TypeExtensions; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; @@ -35,34 +36,44 @@ namespace osu.Game.Rulesets.Objects.Drawables private readonly Lazy> nestedHitObjects = new Lazy>(); public IEnumerable NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : Enumerable.Empty(); - public event Action OnJudgement; - public event Action OnJudgementRemoved; - - public IReadOnlyList Judgements => judgements; - private readonly List judgements = new List(); + /// + /// Invoked when a has been applied by this or a nested . + /// + public event Action OnNewResult; /// - /// Whether a visible judgement should be displayed when this representation is hit. + /// Invoked when a is being reverted by this or a nested . /// - public virtual bool DisplayJudgement => true; + public event Action OnRevertResult; /// - /// Whether this and all of its nested s have been hit. + /// Whether a visual indicator should be displayed when a scoring result occurs. /// - public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && NestedHitObjects.All(n => n.IsHit); + public virtual bool DisplayResult => true; /// /// Whether this and all of its nested s have been judged. /// - public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && NestedHitObjects.All(h => h.AllJudged); + public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged); /// - /// Whether this can be judged. + /// Whether this has been hit. This occurs if is . + /// Note: This does NOT include nested hitobjects. /// - protected virtual bool ProvidesJudgement => true; + public bool IsHit => Result?.IsHit ?? false; + + /// + /// Whether this has been judged. + /// Note: This does NOT include nested hitobjects. + /// + public bool Judged => Result?.HasResult ?? true; + + /// + /// The scoring result of this . + /// + public JudgementResult Result { get; private set; } private bool judgementOccurred; - private bool judgementFinalized => judgements.LastOrDefault()?.Final == true; public bool Interactive = true; public override bool HandleKeyboardInput => Interactive; @@ -82,6 +93,14 @@ namespace osu.Game.Rulesets.Objects.Drawables [BackgroundDependencyLoader] private void load() { + var judgement = HitObject.CreateJudgement(); + if (judgement != null) + { + Result = CreateResult(judgement); + if (Result == null) + throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + } + var samples = GetSamples().ToArray(); if (samples.Any()) @@ -132,18 +151,17 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.Update(); - var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; - - while (judgements.Count > 0) + if (Result != null && Result.HasResult) { - var lastJudgement = judgements[judgements.Count - 1]; - if (lastJudgement.TimeOffset + endTime <= Time.Current) - break; + var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; - judgements.RemoveAt(judgements.Count - 1); - State.Value = ArmedState.Idle; + if (Result.TimeOffset + endTime > Time.Current) + { + OnRevertResult?.Invoke(this, Result); - OnJudgementRemoved?.Invoke(this, lastJudgement); + Result.Type = HitResult.None; + State.Value = ArmedState.Idle; + } } } @@ -151,33 +169,37 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.UpdateAfterChildren(); - UpdateJudgement(false); + UpdateResult(false); } protected virtual void AddNested(DrawableHitObject h) { - h.OnJudgement += (d, j) => OnJudgement?.Invoke(d, j); - h.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(d, j); + h.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r); + h.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r); h.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j); nestedHitObjects.Value.Add(h); } /// - /// Notifies that a new judgement has occurred for this . + /// Applies the of this , notifying responders such as + /// the of the . /// - /// The . - protected void AddJudgement(Judgement judgement) + /// The callback that applies changes to the . + protected void ApplyResult(Action application) { + application?.Invoke(Result); + + 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; - judgement.TimeOffset = Time.Current - endTime; + Result.TimeOffset = Time.Current - endTime; - judgements.Add(judgement); - - switch (judgement.Result) + switch (Result.Type) { case HitResult.None: break; @@ -189,15 +211,15 @@ namespace osu.Game.Rulesets.Objects.Drawables break; } - OnJudgement?.Invoke(this, judgement); + OnNewResult?.Invoke(this, Result); } /// - /// Processes this , checking if any judgements have occurred. + /// Processes this , checking if a scoring result has occurred. /// /// Whether the user triggered this process. - /// Whether a judgement has occurred from this or any nested s. - protected bool UpdateJudgement(bool userTriggered) + /// Whether a scoring result has occurred from this or any nested . + protected bool UpdateResult(bool userTriggered) { judgementOccurred = false; @@ -205,27 +227,35 @@ namespace osu.Game.Rulesets.Objects.Drawables return false; foreach (var d in NestedHitObjects) - judgementOccurred |= d.UpdateJudgement(userTriggered); + judgementOccurred |= d.UpdateResult(userTriggered); - if (!ProvidesJudgement || judgementFinalized || judgementOccurred) + if (judgementOccurred || Judged) return judgementOccurred; var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; - CheckForJudgements(userTriggered, Time.Current - endTime); + CheckForResult(userTriggered, Time.Current - endTime); return judgementOccurred; } /// - /// Checks if any judgements have occurred for this . This method must construct - /// all s and notify of them through . + /// Checks if a scoring result has occurred for this . /// + /// + /// If a scoring result has occurred, this method must invoke to update the result and notify responders. + /// /// Whether the user triggered this check. - /// The offset from the end time at which this check occurred. A > 0 - /// implies that this check occurred after the end time of . - protected virtual void CheckForJudgements(bool userTriggered, double timeOffset) + /// The offset from the end time of the at which this check occurred. + /// A > 0 implies that this check occurred after the end time of the . + protected virtual void CheckForResult(bool userTriggered, double timeOffset) { } + + /// + /// Creates the that represents the scoring result for this . + /// + /// The that provides the scoring information. + protected virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(judgement); } public abstract class DrawableHitObject : DrawableHitObject diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 15c24e2975..beb9620f78 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -9,6 +9,7 @@ using osu.Framework.Lists; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects @@ -105,6 +106,12 @@ namespace osu.Game.Rulesets.Objects protected void AddNested(HitObject hitObject) => nestedHitObjects.Value.Add(hitObject); + /// + /// Creates the that represents the scoring information for this . + /// May be null. + /// + public virtual Judgement CreateJudgement() => null; + /// /// Creates the for this . /// This can be null to indicate that the has no . diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs index 50035ea116..0573a08361 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs @@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch public float X { get; set; } public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index c7451dc978..802080aedb 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -13,21 +13,43 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { - protected override HitObject CreateHit(Vector2 position, bool newCombo) + public ConvertHitObjectParser(double offset, int formatVersion) + : base(offset, formatVersion) { + } + + private bool forceNewCombo; + private int extraComboOffset; + + protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) + { + newCombo |= forceNewCombo; + comboOffset += extraComboOffset; + + forceNewCombo = false; + extraComboOffset = 0; + return new ConvertHit { X = position.X, NewCombo = newCombo, + ComboOffset = comboOffset }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) { + newCombo |= forceNewCombo; + comboOffset += extraComboOffset; + + forceNewCombo = false; + extraComboOffset = 0; + return new ConvertSlider { X = position.X, - NewCombo = newCombo, + NewCombo = FirstObject || newCombo, + ComboOffset = comboOffset, ControlPoints = controlPoints, Distance = length, CurveType = curveType, @@ -36,15 +58,20 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch }; } - protected override HitObject CreateSpinner(Vector2 position, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) { + // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo + // Their combo offset is still added to that next hitobject's combo index + forceNewCombo |= FormatVersion <= 8 || newCombo; + extraComboOffset += comboOffset; + return new ConvertSpinner { EndTime = endTime }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs index 73e277a125..a187caaa26 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs @@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch public float X { get; set; } public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index 9dfe12f25e..db79ca60f1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs @@ -8,10 +8,14 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : HitObject, IHasEndTime + internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasCombo { public double EndTime { get; set; } public double Duration => EndTime - StartTime; + + public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index c48060bfa9..72168a4cd2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -10,6 +10,8 @@ using System.IO; using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Logging; using osu.Framework.MathUtils; namespace osu.Game.Rulesets.Objects.Legacy @@ -19,12 +21,26 @@ namespace osu.Game.Rulesets.Objects.Legacy /// public abstract class ConvertHitObjectParser : HitObjectParser { - public override HitObject Parse(string text) + /// + /// The offset to apply to all time values. + /// + protected readonly double Offset; + + /// + /// The beatmap version. + /// + protected readonly int FormatVersion; + + protected bool FirstObject { get; private set; } = true; + + protected ConvertHitObjectParser(double offset, int formatVersion) { - return Parse(text, 0); + Offset = offset; + FormatVersion = formatVersion; } - public HitObject Parse(string text, double offset) + [CanBeNull] + public override HitObject Parse(string text) { try { @@ -32,7 +48,11 @@ namespace osu.Game.Rulesets.Objects.Legacy Vector2 pos = new Vector2((int)Convert.ToSingle(split[0], CultureInfo.InvariantCulture), (int)Convert.ToSingle(split[1], CultureInfo.InvariantCulture)); - ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]) & ~ConvertHitObjectType.ColourHax; + ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]); + + int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4; + type &= ~ConvertHitObjectType.ComboOffset; + bool combo = type.HasFlag(ConvertHitObjectType.NewCombo); type &= ~ConvertHitObjectType.NewCombo; @@ -43,7 +63,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (type.HasFlag(ConvertHitObjectType.Circle)) { - result = CreateHit(pos, combo); + result = CreateHit(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); @@ -148,11 +168,11 @@ namespace osu.Game.Rulesets.Objects.Legacy for (int i = 0; i < nodes; i++) nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples); + result = CreateSlider(pos, combo, comboOffset, points, length, curveType, repeatCount, nodeSamples); } else if (type.HasFlag(ConvertHitObjectType.Spinner)) { - result = CreateSpinner(new Vector2(512, 384) / 2, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + offset); + result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + Offset); if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); @@ -170,15 +190,20 @@ namespace osu.Game.Rulesets.Objects.Legacy readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); } - result = CreateHold(pos, combo, endTime + offset); + result = CreateHold(pos, combo, comboOffset, endTime + Offset); } if (result == null) - throw new InvalidOperationException($@"Unknown hit object type {type}."); + { + Logger.Log($"Unknown hit object type: {type}. Skipped.", level: LogLevel.Error); + return null; + } - result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + offset; + result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + Offset; result.Samples = convertSoundType(soundType, bankInfo); + FirstObject = false; + return result; } catch (FormatException) @@ -221,37 +246,42 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// The position of the hit object. /// Whether the hit object creates a new combo. + /// When starting a new combo, the offset of the new combo relative to the current one. /// The hit object. - protected abstract HitObject CreateHit(Vector2 position, bool newCombo); + protected abstract HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset); /// /// Creats a legacy Slider-type hit object. /// /// The position of the hit object. /// Whether the hit object creates a new combo. + /// When starting a new combo, the offset of the new combo relative to the current one. /// The slider control points. /// The slider length. /// The slider curve type. /// The slider repeat count. /// The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider. /// The hit object. - protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples); + protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples); /// /// Creates a legacy Spinner-type hit object. /// /// The position of the hit object. + /// Whether the hit object creates a new combo. + /// When starting a new combo, the offset of the new combo relative to the current one. /// The spinner end time. /// The hit object. - protected abstract HitObject CreateSpinner(Vector2 position, double endTime); + protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime); /// /// Creates a legacy Hold-type hit object. /// /// The position of the hit object. /// Whether the hit object creates a new combo. + /// When starting a new combo, the offset of the new combo relative to the current one. /// The hold end time. - protected abstract HitObject CreateHold(Vector2 position, bool newCombo, double endTime); + protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime); private List convertSoundType(LegacySoundType type, SampleBankInfo bankInfo) { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs index c0626c3e56..fa47e56de7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Objects.Legacy Slider = 1 << 1, NewCombo = 1 << 2, Spinner = 1 << 3, - ColourHax = 112, + ComboOffset = 1 << 4 | 1 << 5 | 1 << 6, Hold = 1 << 7 } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs index ea4e7f6907..cbc8d2d4df 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs @@ -8,12 +8,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// /// Legacy osu!mania Hit-type, used for parsing Beatmaps. /// - internal sealed class ConvertHit : HitObject, IHasXPosition, IHasCombo + internal sealed class ConvertHit : HitObject, IHasXPosition { public float X { get; set; } - public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs index 99ba1304e8..6f59965e18 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs @@ -13,21 +13,24 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { - protected override HitObject CreateHit(Vector2 position, bool newCombo) + public ConvertHitObjectParser(double offset, int formatVersion) + : base(offset, formatVersion) + { + } + + protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) { return new ConvertHit { - X = position.X, - NewCombo = newCombo, + X = position.X }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) { return new ConvertSlider { X = position.X, - NewCombo = newCombo, ControlPoints = controlPoints, Distance = length, CurveType = curveType, @@ -36,7 +39,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania }; } - protected override HitObject CreateSpinner(Vector2 position, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) { return new ConvertSpinner { @@ -45,7 +48,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) { return new ConvertHold { diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs index a8d7b23df1..e1572889a3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs @@ -8,12 +8,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// /// Legacy osu!mania Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition, IHasCombo + internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition { public float X { get; set; } - public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs index f015272b2c..0062e83a28 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } + public int ComboOffset { get; set; } + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index 801e4ea449..acd0de8688 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -14,21 +14,43 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { - protected override HitObject CreateHit(Vector2 position, bool newCombo) + public ConvertHitObjectParser(double offset, int formatVersion) + : base(offset, formatVersion) { + } + + private bool forceNewCombo; + private int extraComboOffset; + + protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) + { + newCombo |= forceNewCombo; + comboOffset += extraComboOffset; + + forceNewCombo = false; + extraComboOffset = 0; + return new ConvertHit { Position = position, - NewCombo = newCombo, + NewCombo = FirstObject || newCombo, + ComboOffset = comboOffset }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) { + newCombo |= forceNewCombo; + comboOffset += extraComboOffset; + + forceNewCombo = false; + extraComboOffset = 0; + return new ConvertSlider { Position = position, - NewCombo = newCombo, + NewCombo = FirstObject || newCombo, + ComboOffset = comboOffset, ControlPoints = controlPoints, Distance = Math.Max(0, length), CurveType = curveType, @@ -37,8 +59,13 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu }; } - protected override HitObject CreateSpinner(Vector2 position, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) { + // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo + // Their combo offset is still added to that next hitobject's combo index + forceNewCombo |= FormatVersion <= 8 || newCombo; + extraComboOffset += comboOffset; + return new ConvertSpinner { Position = position, @@ -46,7 +73,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index ec5a002bbb..45f7bc9e67 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } + public int ComboOffset { get; set; } + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index 0141785f31..3b349d9704 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasPosition + internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasPosition, IHasCombo { public double EndTime { get; set; } @@ -22,5 +22,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float Y => Position.Y; protected override HitWindows CreateHitWindows() => null; + + public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs index 5e9786c84a..66e504bf22 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs @@ -1,17 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Rulesets.Objects.Types; - namespace osu.Game.Rulesets.Objects.Legacy.Taiko { /// /// Legacy osu!taiko Hit-type, used for parsing Beatmaps. /// - internal sealed class ConvertHit : HitObject, IHasCombo + internal sealed class ConvertHit : HitObject { - public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs index 03b1a3187a..e5904825c2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs @@ -13,19 +13,20 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { - protected override HitObject CreateHit(Vector2 position, bool newCombo) + public ConvertHitObjectParser(double offset, int formatVersion) + : base(offset, formatVersion) { - return new ConvertHit - { - NewCombo = newCombo, - }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) + protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) + { + return new ConvertHit(); + } + + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) { return new ConvertSlider { - NewCombo = newCombo, ControlPoints = controlPoints, Distance = length, CurveType = curveType, @@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko }; } - protected override HitObject CreateSpinner(Vector2 position, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) { return new ConvertSpinner { @@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs index 8a9a0db0a7..11c0a2baae 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs @@ -1,17 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Rulesets.Objects.Types; - namespace osu.Game.Rulesets.Objects.Legacy.Taiko { /// /// Legacy osu!taiko Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasCombo + internal sealed class ConvertSlider : Legacy.ConvertSlider { - public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs index cb8b6f495a..95f1a1cb3d 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs @@ -12,5 +12,10 @@ namespace osu.Game.Rulesets.Objects.Types /// Whether the HitObject starts a new combo. /// bool NewCombo { get; } + + /// + /// When starting a new combo, the offset of the new combo relative to the current one. + /// + int ComboOffset { get; } } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9e8ea0f7c2..b0cea7009e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Configuration; +using osu.Framework.Extensions.TypeExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by the . /// - public event Action NewJudgement; + public event Action NewJudgement; /// /// Additional conditions on top of that cause a failing state. @@ -144,9 +145,10 @@ namespace osu.Game.Rulesets.Scoring /// Notifies subscribers of that a new judgement has occurred. /// /// The judgement to notify subscribers of. - protected void NotifyNewJudgement(Judgement judgement) + /// The judgement scoring result to notify subscribers of. + protected void NotifyNewJudgement(JudgementResult result) { - NewJudgement?.Invoke(judgement); + NewJudgement?.Invoke(result); if (HasCompleted) AllJudged?.Invoke(); @@ -194,9 +196,10 @@ namespace osu.Game.Rulesets.Scoring { Debug.Assert(base_portion + combo_portion == 1.0); - rulesetContainer.OnJudgement += AddJudgement; - rulesetContainer.OnJudgementRemoved += RemoveJudgement; + rulesetContainer.OnNewResult += applyResult; + rulesetContainer.OnRevertResult += revertResult; + ApplyBeatmap(rulesetContainer.Beatmap); SimulateAutoplay(rulesetContainer.Beatmap); Reset(true); @@ -210,46 +213,80 @@ namespace osu.Game.Rulesets.Scoring } /// - /// Simulates an autoplay of s that will be judged by this - /// by adding s for each in the . - /// - /// This is required for to work, otherwise will be used. - /// + /// Applies any properties of the which affect scoring to this . /// - /// The containing the s that will be judged by this . - protected virtual void SimulateAutoplay(Beatmap beatmap) { } + /// The to read properties from. + protected virtual void ApplyBeatmap(Beatmap beatmap) + { + } /// - /// Adds a judgement to this ScoreProcessor. + /// Simulates an autoplay of the to determine scoring values. /// - /// The judgement to add. - protected void AddJudgement(Judgement judgement) + /// This provided temporarily. DO NOT USE. + /// The to simulate. + protected virtual void SimulateAutoplay(Beatmap beatmap) { - OnNewJudgement(judgement); + foreach (var obj in beatmap.HitObjects) + simulate(obj); + + void simulate(HitObject obj) + { + foreach (var nested in obj.NestedHitObjects) + simulate(nested); + + var judgement = obj.CreateJudgement(); + if (judgement == null) + return; + + var result = CreateResult(judgement); + if (result == null) + throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + + result.Type = judgement.MaxResult; + + applyResult(result); + } + } + + /// + /// Applies the score change of a to this . + /// + /// The to apply. + private void applyResult(JudgementResult result) + { + ApplyResult(result); updateScore(); UpdateFailed(); - NotifyNewJudgement(judgement); + NotifyNewJudgement(result); } - protected void RemoveJudgement(Judgement judgement) + /// + /// Reverts the score change of a that was applied to this . + /// + /// The judgement to remove. + /// The judgement scoring result. + private void revertResult(JudgementResult result) { - OnJudgementRemoved(judgement); + RevertResult(result); updateScore(); } /// - /// Applies a judgement. + /// Applies the score change of a to this . /// - /// The judgement to apply/ - protected virtual void OnNewJudgement(Judgement judgement) + /// The to apply. + protected virtual void ApplyResult(JudgementResult result) { - judgement.ComboAtJudgement = Combo; - judgement.HighestComboAtJudgement = HighestCombo; + result.ComboAtJudgement = Combo; + result.HighestComboAtJudgement = HighestCombo; - if (judgement.AffectsCombo) + JudgedHits++; + + if (result.Judgement.AffectsCombo) { - switch (judgement.Result) + switch (result.Type) { case HitResult.None: break; @@ -260,43 +297,41 @@ namespace osu.Game.Rulesets.Scoring Combo.Value++; break; } - - JudgedHits++; } - if (judgement.IsBonus) + if (result.Judgement.IsBonus) { - if (judgement.IsHit) - bonusScore += judgement.NumericResult; + if (result.IsHit) + bonusScore += result.Judgement.NumericResultFor(result); } else { - baseScore += judgement.NumericResult; - rollingMaxBaseScore += judgement.MaxNumericResult; + baseScore += result.Judgement.NumericResultFor(result); + rollingMaxBaseScore += result.Judgement.MaxNumericResult; } } /// - /// Removes a judgement. This should reverse everything in . + /// Reverts the score change of a that was applied to this . /// /// The judgement to remove. - protected virtual void OnJudgementRemoved(Judgement judgement) + /// The judgement scoring result. + protected virtual void RevertResult(JudgementResult result) { - Combo.Value = judgement.ComboAtJudgement; - HighestCombo.Value = judgement.HighestComboAtJudgement; + Combo.Value = result.ComboAtJudgement; + HighestCombo.Value = result.HighestComboAtJudgement; - if (judgement.AffectsCombo) - JudgedHits--; + JudgedHits--; - if (judgement.IsBonus) + if (result.Judgement.IsBonus) { - if (judgement.IsHit) - bonusScore -= judgement.NumericResult; + if (result.IsHit) + bonusScore -= result.Judgement.NumericResultFor(result); } else { - baseScore -= judgement.NumericResult; - rollingMaxBaseScore -= judgement.MaxNumericResult; + baseScore -= result.Judgement.NumericResultFor(result); + rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } } @@ -333,6 +368,12 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore = 0; bonusScore = 0; } + + /// + /// Creates the that represents the scoring result for a . + /// + /// The that provides the scoring information. + protected virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(judgement); } public enum ScoringMode diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 2f44d99e18..e090a18eda 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -1,26 +1,37 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Configuration; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.UI { public abstract class Playfield : ScalableContainer { /// - /// The HitObjects contained in this Playfield. + /// The contained in this Playfield. /// - public HitObjectContainer HitObjects { get; private set; } + public HitObjectContainer HitObjectContainer { get; private set; } /// - /// All the s nested inside this playfield. + /// All the s contained in this and all . /// - public IReadOnlyList NestedPlayfields => nestedPlayfields; - private List nestedPlayfields; + public IEnumerable AllHitObjects => HitObjectContainer?.Objects.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)) ?? Enumerable.Empty(); + + /// + /// All s nested inside this . + /// + public IEnumerable NestedPlayfields => nestedPlayfields.IsValueCreated ? nestedPlayfields.Value : Enumerable.Empty(); + + private readonly Lazy> nestedPlayfields = new Lazy>(); /// /// Whether judgements should be displayed by this and and all nested s. @@ -42,31 +53,35 @@ namespace osu.Game.Rulesets.UI RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load() - { - HitObjects = CreateHitObjectContainer(); - HitObjects.RelativeSizeAxes = Axes.Both; + private WorkingBeatmap beatmap; - Add(HitObjects); + [BackgroundDependencyLoader] + private void load(IBindableBeatmap beatmap) + { + this.beatmap = beatmap.Value; + + HitObjectContainer = CreateHitObjectContainer(); + HitObjectContainer.RelativeSizeAxes = Axes.Both; + + Add(HitObjectContainer); } /// /// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield. /// - public virtual void PostProcess() => nestedPlayfields?.ForEach(p => p.PostProcess()); + public virtual void PostProcess() => NestedPlayfields.ForEach(p => p.PostProcess()); /// /// Adds a DrawableHitObject to this Playfield. /// /// The DrawableHitObject to add. - public virtual void Add(DrawableHitObject h) => HitObjects.Add(h); + public virtual void Add(DrawableHitObject h) => HitObjectContainer.Add(h); /// /// Remove a DrawableHitObject from this Playfield. /// /// The DrawableHitObject to remove. - public virtual void Remove(DrawableHitObject h) => HitObjects.Remove(h); + public virtual void Remove(DrawableHitObject h) => HitObjectContainer.Remove(h); /// /// Registers a as a nested . @@ -75,17 +90,23 @@ namespace osu.Game.Rulesets.UI /// The to add. protected void AddNested(Playfield otherPlayfield) { - if (nestedPlayfields == null) - nestedPlayfields = new List(); - - nestedPlayfields.Add(otherPlayfield); - otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); + nestedPlayfields.Value.Add(otherPlayfield); } /// /// Creates the container that will be used to contain the s. /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); + + protected override void Update() + { + base.Update(); + + if (beatmap != null) + foreach (var mod in beatmap.Mods.Value) + if (mod is IUpdatableByPlayfield updatable) + updatable.Update(this); + } } } diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index ee34e2df04..a830803fb1 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -182,8 +182,15 @@ namespace osu.Game.Rulesets.UI public abstract class RulesetContainer : RulesetContainer where TObject : HitObject { - public event Action OnJudgement; - public event Action OnJudgementRemoved; + /// + /// Invoked when a has been applied by a . + /// + public event Action OnNewResult; + + /// + /// Invoked when a is being reverted by a . + /// + public event Action OnRevertResult; /// /// The Beatmap @@ -290,8 +297,8 @@ namespace osu.Game.Rulesets.UI if (drawableObject == null) continue; - drawableObject.OnJudgement += (d, j) => OnJudgement?.Invoke(j); - drawableObject.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(j); + drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r); + drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r); Playfield.Add(drawableObject); } @@ -299,7 +306,7 @@ namespace osu.Game.Rulesets.UI Playfield.PostProcess(); foreach (var mod in Mods.OfType()) - mod.ApplyToDrawableHitObjects(Playfield.HitObjects.Objects); + mod.ApplyToDrawableHitObjects(Playfield.HitObjectContainer.Objects); } protected override void Update() diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 7146ad8064..ec73c0fb14 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// The container that contains the s. /// - public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects; + public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)HitObjectContainer; /// /// The direction in which s in this should scroll. diff --git a/osu.Game/Screens/Edit/Components/CircularButton.cs b/osu.Game/Screens/Edit/Components/CircularButton.cs new file mode 100644 index 0000000000..a8ad242772 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/CircularButton.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Graphics.UserInterface; +using OpenTK; + +namespace osu.Game.Screens.Edit.Components +{ + public class CircularButton : OsuButton + { + private const float width = 125; + private const float height = 30; + + public CircularButton() + { + Size = new Vector2(width, height); + } + + protected override void Update() + { + base.Update(); + Content.CornerRadius = DrawHeight / 2f; + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs index 03ac8e91f0..d212bbe7dd 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -9,30 +8,24 @@ using osu.Framework.Input.EventArgs; using osu.Framework.Input.States; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Edit.Screens.Compose.Layers { public class HitObjectMaskLayer : CompositeDrawable { - private readonly Playfield playfield; - private readonly HitObjectComposer composer; - private MaskContainer maskContainer; + private HitObjectComposer composer; - public HitObjectMaskLayer(Playfield playfield, HitObjectComposer composer) + public HitObjectMaskLayer() { - // we need the playfield as HitObjects may not be initialised until its BDL. - this.playfield = playfield; - - this.composer = composer; - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load() + private void load(HitObjectComposer composer) { + this.composer = composer; + maskContainer = new MaskContainer(); var maskSelection = composer.CreateMaskSelection(); @@ -55,7 +48,7 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers dragLayer.CreateProxy() }; - foreach (var obj in playfield.HitObjects.Objects) + foreach (var obj in composer.HitObjects) addMask(obj); } @@ -77,18 +70,5 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers maskContainer.Add(mask); } - - /// - /// Removes the mask for a . - /// - /// The to remove the mask for. - private void removeMask(DrawableHitObject hitObject) - { - var mask = maskContainer.FirstOrDefault(h => h.HitObject == hitObject); - if (mask == null) - return; - - maskContainer.Remove(mask); - } } } diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index c3b3e747fd..3cef20e510 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shaders; using osu.Game.Screens.Menu; using OpenTK; using osu.Framework.Screens; +using osu.Game.Overlays; namespace osu.Game.Screens { @@ -18,6 +19,8 @@ namespace osu.Game.Screens protected override bool HideOverlaysOnEnter => true; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; + protected override bool AllowBackButton => false; public Loader() diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index ce00686c02..b9a799328e 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -174,6 +174,9 @@ namespace osu.Game.Screens.Menu ButtonSystemState lastState = state; state = value; + if (game != null) + game.OverlayActivationMode.Value = state == ButtonSystemState.Exit ? OverlayActivation.Disabled : OverlayActivation.All; + updateLogoState(lastState); Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}"); @@ -205,11 +208,7 @@ namespace osu.Game.Screens.Menu { logoTracking = false; - if (game != null) - { - game.OverlayActivationMode.Value = state == ButtonSystemState.Exit ? OverlayActivation.Disabled : OverlayActivation.All; - game.Toolbar.Hide(); - } + game?.Toolbar.Hide(); logo.ClearTransforms(targetMember: nameof(Position)); logo.RelativePositionAxes = Axes.Both; @@ -243,11 +242,7 @@ namespace osu.Game.Screens.Menu if (impact) logo.Impact(); - if (game != null) - { - game.OverlayActivationMode.Value = OverlayActivation.All; - game.Toolbar.State = Visibility.Visible; - } + game?.Toolbar.Show(); }, 200); break; default: @@ -278,7 +273,7 @@ namespace osu.Game.Screens.Menu if (logo != null) { - if (logoTracking && iconFacade.IsLoaded) + if (logoTracking && logo.RelativePositionAxes == Axes.None && iconFacade.IsLoaded) logo.Position = logoTrackingPosition; iconFacade.Width = logo.SizeForFlow * 0.5f; diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index fb6130fa36..a3cb2f13d0 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -176,7 +176,7 @@ namespace osu.Game.Screens.Menu Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; - ColourInfo colourInfo = DrawInfo.Colour; + ColourInfo colourInfo = DrawColourInfo.Colour; colourInfo.ApplyChild(Colour); if (AudioData != null) diff --git a/osu.Game/Screens/Play/Break/BreakArrows.cs b/osu.Game/Screens/Play/Break/BreakArrows.cs index 4a4a7960fa..1382aa9d9d 100644 --- a/osu.Game/Screens/Play/Break/BreakArrows.cs +++ b/osu.Game/Screens/Play/Break/BreakArrows.cs @@ -31,23 +31,30 @@ namespace osu.Game.Screens.Play.Break RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] { - leftGlowIcon = new GlowIcon + new ParallaxContainer { - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight, - X = -glow_icon_offscreen_offset, - Icon = Graphics.FontAwesome.fa_chevron_right, - BlurSigma = new Vector2(glow_icon_blur_sigma), - Size = new Vector2(glow_icon_size), - }, - rightGlowIcon = new GlowIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft, - X = glow_icon_offscreen_offset, - Icon = Graphics.FontAwesome.fa_chevron_left, - BlurSigma = new Vector2(glow_icon_blur_sigma), - Size = new Vector2(glow_icon_size), + ParallaxAmount = -0.01f, + Children = new Drawable[] + { + leftGlowIcon = new GlowIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + X = -glow_icon_offscreen_offset, + Icon = Graphics.FontAwesome.fa_chevron_right, + BlurSigma = new Vector2(glow_icon_blur_sigma), + Size = new Vector2(glow_icon_size), + }, + rightGlowIcon = new GlowIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + X = glow_icon_offscreen_offset, + Icon = Graphics.FontAwesome.fa_chevron_left, + BlurSigma = new Vector2(glow_icon_blur_sigma), + Size = new Vector2(glow_icon_size), + }, + } }, new ParallaxContainer { diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 894322dd41..1a164b473d 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -63,6 +63,12 @@ namespace osu.Game.Screens.Play.HUD }; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + mods.UnbindAll(); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs index ab446550a6..b551b1f7a6 100644 --- a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs @@ -92,9 +92,9 @@ namespace osu.Game.Screens.Play.HUD }; } - public void Flash(Judgement judgement) + public void Flash(JudgementResult result) { - if (judgement.Result == HitResult.Miss) + if (result.Type == HitResult.Miss) return; fill.FadeEdgeEffectTo(Math.Min(1, fill.EdgeEffect.Colour.Linear.A + (1f - base_glow_opacity) / glow_max_hits), 50, Easing.OutQuint) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2e23bb16f0..5ad0130fd7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -124,7 +124,7 @@ namespace osu.Game.Screens.Play if (!RulesetContainer.Objects.Any()) { - Logger.Error(new InvalidOperationException("Beatmap contains no hit objects!"), "Beatmap contains no hit objects!"); + Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error); return; } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index e9aa012cd7..fd4322c268 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -14,9 +14,11 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Screens.Play { @@ -69,21 +71,25 @@ namespace osu.Game.Screens.Play } }); - loadTask = LoadComponentAsync(player); + loadTask = LoadComponentAsync(player, playerLoaded); } + private void playerLoaded(Player player) => info.Loading = false; + protected override void OnResuming(Screen last) { base.OnResuming(last); contentIn(); + info.Loading = true; + //we will only be resumed if the player has requested a re-run (see ValidForResume setting above) loadTask = LoadComponentAsync(player = new Player { RestartCount = player.RestartCount + 1, RestartRequested = player.RestartRequested, - }); + }, playerLoaded); this.Delay(400).Schedule(pushWhenLoaded); } @@ -258,6 +264,25 @@ namespace osu.Game.Screens.Play } private readonly WorkingBeatmap beatmap; + private LoadingAnimation loading; + private Sprite backgroundSprite; + + public bool Loading + { + set + { + if (value) + { + loading.Show(); + backgroundSprite.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); + } + else + { + loading.Hide(); + backgroundSprite.FadeColour(Color4.White, 400, Easing.OutQuint); + } + } + } public BeatmapMetadataDisplay(WorkingBeatmap beatmap) { @@ -304,9 +329,9 @@ namespace osu.Game.Screens.Play Anchor = Anchor.TopCentre, CornerRadius = 10, Masking = true, - Children = new[] + Children = new Drawable[] { - new Sprite + backgroundSprite = new Sprite { RelativeSizeAxes = Axes.Both, Texture = beatmap?.Background, @@ -314,6 +339,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, FillMode = FillMode.Fill, }, + loading = new LoadingAnimation { Scale = new Vector2(1.3f) } } }, new OsuSpriteText @@ -341,6 +367,8 @@ namespace osu.Game.Screens.Play }, } }; + + Loading = true; } } } diff --git a/osu.Game/Screens/Ranking/ResultsPageScore.cs b/osu.Game/Screens/Ranking/ResultsPageScore.cs index 42d8af07b9..040458e0eb 100644 --- a/osu.Game/Screens/Ranking/ResultsPageScore.cs +++ b/osu.Game/Screens/Ranking/ResultsPageScore.cs @@ -29,6 +29,7 @@ namespace osu.Game.Screens.Ranking { public class ResultsPageScore : ResultsPage { + private Container scoreContainer; private ScoreCounter scoreCounter; public ResultsPageScore(Score score, WorkingBeatmap beatmap) : base(score, beatmap) { } @@ -76,7 +77,7 @@ namespace osu.Game.Screens.Ranking Size = new Vector2(150, 60), Margin = new MarginPadding(20), }, - new Container + scoreContainer = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -92,8 +93,8 @@ namespace osu.Game.Screens.Ranking }, scoreCounter = new SlowScoreCounter(6) { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Colour = colours.PinkDarker, Y = 10, TextSize = 56, @@ -185,6 +186,13 @@ namespace osu.Game.Screens.Ranking }); } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + scoreCounter.Scale = new Vector2(Math.Min(1f, (scoreContainer.DrawWidth - 20) / scoreCounter.DrawWidth)); + } + private class DrawableScoreStatistic : Container { private readonly KeyValuePair statistic; @@ -368,7 +376,7 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { if (!string.IsNullOrEmpty(user.CoverUrl)) cover.Texture = textures.Get(user.CoverUrl); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6e4454a311..b6cbaf45e9 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -64,32 +64,36 @@ namespace osu.Game.Screens.Select public IEnumerable BeatmapSets { - get { return beatmapSets.Select(g => g.BeatmapSet); } - set + get => beatmapSets.Select(g => g.BeatmapSet); + set => loadBeatmapSets(() => value); + } + + public void LoadBeatmapSetsFromManager(BeatmapManager manager) => loadBeatmapSets(manager.GetAllUsableBeatmapSetsEnumerable); + + private void loadBeatmapSets(Func> beatmapSets) + { + CarouselRoot newRoot = new CarouselRoot(this); + + Task.Run(() => { - CarouselRoot newRoot = new CarouselRoot(this); + beatmapSets().Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild); + newRoot.Filter(activeCriteria); - Task.Run(() => + // preload drawables as the ctor overhead is quite high currently. + var _ = newRoot.Drawables; + }).ContinueWith(_ => Schedule(() => + { + root = newRoot; + scrollableContent.Clear(false); + itemsCache.Invalidate(); + scrollPositionCache.Invalidate(); + + Schedule(() => { - value.Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild); - newRoot.Filter(activeCriteria); - - // preload drawables as the ctor overhead is quite high currently. - var _ = newRoot.Drawables; - }).ContinueWith(_ => Schedule(() => - { - root = newRoot; - scrollableContent.Clear(false); - itemsCache.Invalidate(); - scrollPositionCache.Invalidate(); - - Schedule(() => - { - BeatmapSetsChanged?.Invoke(); - initialLoadComplete = true; - }); - })); - } + BeatmapSetsChanged?.Invoke(); + initialLoadComplete = true; + }); + })); } private readonly List yPositions = new List(); @@ -482,6 +486,15 @@ namespace osu.Game.Screens.Select updateItem(p, halfHeight); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + // aggressively dispose "off-screen" items to reduce GC pressure. + foreach (var i in Items) + i.Dispose(); + } + private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet) { if (beatmapSet.Beatmaps.All(b => b.Hidden)) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index d554a22735..23f338b530 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -50,12 +50,12 @@ namespace osu.Game.Screens.Select.Carousel Children = new Drawable[] { - new DelayedLoadWrapper( + new DelayedLoadUnloadWrapper(() => new PanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, OnLoadComplete = d => d.FadeInFromZero(1000, Easing.OutQuint), - }, 300 + }, 300, 5000 ), new FillFlowContainer { diff --git a/osu.Game/Screens/Select/ImportFromStablePopup.cs b/osu.Game/Screens/Select/ImportFromStablePopup.cs index 7c3b98cc2e..a52b5bab2a 100644 --- a/osu.Game/Screens/Select/ImportFromStablePopup.cs +++ b/osu.Game/Screens/Select/ImportFromStablePopup.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Select public ImportFromStablePopup(Action importFromStable) { HeaderText = @"You have no beatmaps!"; - BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps?"; + BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps (and skins)?"; Icon = FontAwesome.fa_plane; diff --git a/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs b/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs index 0c4b369f36..0cf1e60304 100644 --- a/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs +++ b/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs @@ -41,7 +41,10 @@ namespace osu.Game.Screens.Select.Leaderboards updateTexture(); } - private void updateTexture() => rankSprite.Texture = textures.Get($@"Grades/{Rank.GetDescription()}"); + private void updateTexture() + { + rankSprite.Texture = textures.Get($@"Grades/{Rank.GetDescription()}"); + } public void UpdateRank(ScoreRank newRank) { diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index a346911ca2..2c43b333aa 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using OpenTK.Input; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -20,6 +19,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Edit; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Skinning; namespace osu.Game.Screens.Select { @@ -50,13 +50,14 @@ namespace osu.Game.Screens.Select private SampleChannel sampleConfirm; - public readonly Bindable> SelectedMods = new Bindable>(new List()); + [Cached] + [Cached(Type = typeof(IBindable>))] + private readonly Bindable> selectedMods = new Bindable>(new Mod[] { }); [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, OsuGame osu) + private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, SkinManager skins, DialogOverlay dialogOverlay, Bindable> selectedMods) { - if (osu != null) SelectedMods.BindTo(osu.SelectedMods); - modSelect.SelectedMods.BindTo(SelectedMods); + if (selectedMods != null) this.selectedMods.BindTo(selectedMods); sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); @@ -77,14 +78,17 @@ namespace osu.Game.Screens.Select // if we have no beatmaps but osu-stable is found, let's prompt the user to import. if (!beatmaps.GetAllUsableBeatmapSets().Any() && beatmaps.StableInstallationAvailable) dialogOverlay.Push(new ImportFromStablePopup(() => - Task.Factory.StartNew(beatmaps.ImportFromStable, TaskCreationOptions.LongRunning))); + { + beatmaps.ImportFromStableAsync(); + skins.ImportFromStableAsync(); + })); }); } } protected override void UpdateBeatmap(WorkingBeatmap beatmap) { - beatmap.Mods.BindTo(SelectedMods); + beatmap.Mods.BindTo(selectedMods); base.UpdateBeatmap(beatmap); @@ -131,7 +135,7 @@ namespace osu.Game.Screens.Select if (Beatmap.Value.Track != null) Beatmap.Value.Track.Looping = false; - SelectedMods.UnbindAll(); + selectedMods.UnbindAll(); Beatmap.Value.Mods.Value = new Mod[] { }; return false; @@ -147,10 +151,10 @@ namespace osu.Game.Screens.Select var auto = Ruleset.Value.CreateInstance().GetAutoplayMod(); var autoType = auto.GetType(); - var mods = modSelect.SelectedMods.Value; + var mods = selectedMods.Value; if (mods.All(m => m.GetType() != autoType)) { - modSelect.SelectedMods.Value = mods.Append(auto); + selectedMods.Value = mods.Append(auto); removeAutoModOnResume = true; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 1bcd65e30b..efdf55e477 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -221,7 +221,7 @@ namespace osu.Game.Screens.Select sampleChangeDifficulty = audio.Sample.Get(@"SongSelect/select-difficulty"); sampleChangeBeatmap = audio.Sample.Get(@"SongSelect/select-expand"); - Carousel.BeatmapSets = this.beatmaps.GetAllUsableBeatmapSetsEnumerable(); + Carousel.LoadBeatmapSetsFromManager(this.beatmaps); } public void Edit(BeatmapInfo beatmap) @@ -314,13 +314,13 @@ namespace osu.Game.Screens.Select { Logger.Log($"updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ID.ToString() ?? "null"}"); - WorkingBeatmap working = Beatmap.Value; - bool preview = false; if (ruleset?.Equals(Ruleset.Value) == false) { Logger.Log($"ruleset changed from \"{Ruleset.Value}\" to \"{ruleset}\""); + + Beatmap.Value.Mods.Value = Enumerable.Empty(); Ruleset.Value = ruleset; // force a filter before attempting to change the beatmap. @@ -340,7 +340,7 @@ namespace osu.Game.Screens.Select Logger.Log($"beatmap changed from \"{Beatmap.Value.BeatmapInfo}\" to \"{beatmap}\""); preview = beatmap?.BeatmapSetInfoID != Beatmap.Value?.BeatmapInfo.BeatmapSetInfoID; - working = beatmaps.GetWorkingBeatmap(beatmap, Beatmap.Value); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap.Value); if (beatmap != null) { @@ -351,9 +351,6 @@ namespace osu.Game.Screens.Select } } - working.Mods.Value = Enumerable.Empty(); - Beatmap.Value = working; - ensurePlayingSelected(preview); UpdateBeatmap(Beatmap.Value); } @@ -463,6 +460,8 @@ namespace osu.Game.Screens.Select { base.Dispose(isDisposing); + Ruleset.UnbindAll(); + if (beatmaps != null) { beatmaps.ItemAdded -= onBeatmapSetAdded; diff --git a/osu.Game/Screens/Tournament/Drawings.cs b/osu.Game/Screens/Tournament/Drawings.cs index 63d29d5cd7..4e2109afd0 100644 --- a/osu.Game/Screens/Tournament/Drawings.cs +++ b/osu.Game/Screens/Tournament/Drawings.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Tournament TextureStore flagStore = new TextureStore(); // Local flag store - flagStore.AddStore(new RawTextureLoaderStore(new NamespacedResourceStore(new StorageBackedResourceStore(storage), "Drawings"))); + flagStore.AddStore(new TextureLoaderStore(new NamespacedResourceStore(new StorageBackedResourceStore(storage), "Drawings"))); // Default texture store flagStore.AddStore(textures); diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 45c6ec80aa..ce7edf8683 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; @@ -36,7 +37,7 @@ namespace osu.Game.Skinning Configuration = new SkinConfiguration(); Samples = audioManager.GetSampleManager(storage); - Textures = new TextureStore(new RawTextureLoaderStore(storage)); + Textures = new TextureStore(new TextureLoaderStore(storage)); } public override Drawable GetDrawableComponent(string componentName) @@ -108,10 +109,12 @@ namespace osu.Game.Skinning return path == null ? null : underlyingStore.GetStream(path); } - byte[] IResourceStore.Get(string name) + byte[] IResourceStore.Get(string name) => GetAsync(name).Result; + + public Task GetAsync(string name) { string path = getPathForFile(name); - return path == null ? null : underlyingStore.Get(path); + return path == null ? Task.FromResult(null) : underlyingStore.GetAsync(path); } #region IDisposable Support diff --git a/osu.Game/Skinning/LocalSkinOverrideContainer.cs b/osu.Game/Skinning/LocalSkinOverrideContainer.cs index 3adf287cf7..25d9442e6f 100644 --- a/osu.Game/Skinning/LocalSkinOverrideContainer.cs +++ b/osu.Game/Skinning/LocalSkinOverrideContainer.cs @@ -85,12 +85,10 @@ namespace osu.Game.Skinning private void load(OsuConfigManager config) { beatmapSkins = config.GetBindable(OsuSetting.BeatmapSkins); - beatmapSkins.ValueChanged += val => onSourceChanged(); - beatmapSkins.TriggerChange(); + beatmapSkins.BindValueChanged(_ => onSourceChanged()); beatmapHitsounds = config.GetBindable(OsuSetting.BeatmapHitsounds); - beatmapHitsounds.ValueChanged += val => onSourceChanged(); - beatmapHitsounds.TriggerChange(); + beatmapHitsounds.BindValueChanged(_ => onSourceChanged(), true); } protected override void LoadComplete() diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 2387924cfa..bd694e443a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -26,17 +26,25 @@ namespace osu.Game.Skinning public override string[] HandledExtensions => new[] { ".osk" }; + protected override string ImportFromStablePath => "Skins"; + /// - /// Returns a list of all usable s. + /// Returns a list of all usable s. Includes the special default skin plus all skins from . /// /// A list of available . public List GetAllUsableSkins() { - var userSkins = ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + var userSkins = GetAllUserSkins(); userSkins.Insert(0, SkinInfo.Default); return userSkins; } + /// + /// Returns a list of all usable s that have been loaded by the user. + /// + /// A list of available . + public List GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name @@ -85,6 +93,13 @@ namespace osu.Game.Skinning { this.audio = audio; + ItemRemoved += removedInfo => + { + // check the removed skin is not the current user choice. if it is, switch back to default. + if (removedInfo.ID == CurrentSkinInfo.Value.ID) + CurrentSkinInfo.Value = SkinInfo.Default; + }; + CurrentSkinInfo.ValueChanged += info => CurrentSkin.Value = GetSkin(info); CurrentSkin.ValueChanged += skin => { @@ -93,16 +108,6 @@ namespace osu.Game.Skinning SourceChanged?.Invoke(); }; - - // migrate older imports which didn't have access to skin.ini - using (ContextFactory.GetForWrite()) - { - foreach (var skinInfo in ModelStore.ConsumableItems.Where(s => s.Name.EndsWith(".osk"))) - { - populate(skinInfo); - Update(skinInfo); - } - } } /// diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index f1ee0db6ce..0b94697405 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -52,5 +52,13 @@ namespace osu.Game.Skinning protected virtual void SkinChanged(ISkinSource skin, bool allowFallback) { } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skin != null) + skin.SourceChanged -= onChange; + } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index d746bb90c4..37c198f370 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -57,7 +57,7 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader] private void load(FileStore fileStore) { - dependencies.Cache(new TextureStore(new RawTextureLoaderStore(fileStore.Store), false) { ScaleAdjust = 1, }); + dependencies.Cache(new TextureStore(new TextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) Add(layer.CreateDrawable()); diff --git a/osu.Game/Users/Avatar.cs b/osu.Game/Users/Avatar.cs index 9ba9549164..e6e1ba3c53 100644 --- a/osu.Game/Users/Avatar.cs +++ b/osu.Game/Users/Avatar.cs @@ -24,7 +24,7 @@ namespace osu.Game.Users } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { if (textures == null) throw new ArgumentNullException(nameof(textures)); diff --git a/osu.Game/Users/UserCoverBackground.cs b/osu.Game/Users/UserCoverBackground.cs index 58b92b2750..fddbd57f9c 100644 --- a/osu.Game/Users/UserCoverBackground.cs +++ b/osu.Game/Users/UserCoverBackground.cs @@ -18,7 +18,7 @@ namespace osu.Game.Users } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { if (textures == null) throw new ArgumentNullException(nameof(textures)); diff --git a/osu.Game/Utils/RavenLogger.cs b/osu.Game/Utils/RavenLogger.cs new file mode 100644 index 0000000000..b28dd1fb73 --- /dev/null +++ b/osu.Game/Utils/RavenLogger.cs @@ -0,0 +1,89 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using osu.Framework.Logging; +using SharpRaven; +using SharpRaven.Data; + +namespace osu.Game.Utils +{ + /// + /// Report errors to sentry. + /// + public class RavenLogger : IDisposable + { + private readonly RavenClient raven = new RavenClient("https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255"); + + private readonly List tasks = new List(); + + private Exception lastException; + + public RavenLogger(OsuGame game) + { + raven.Release = game.Version; + + if (!game.IsDeployedBuild) return; + + Logger.NewEntry += entry => + { + if (entry.Level < LogLevel.Verbose) return; + + var exception = entry.Exception; + + if (exception != null) + { + // since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports. + if (lastException != null && + lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace)) + { + return; + } + + lastException = exception; + queuePendingTask(raven.CaptureAsync(new SentryEvent(exception))); + } + else + raven.AddTrail(new Breadcrumb(entry.Target.ToString(), BreadcrumbType.Navigation) { Message = entry.Message }); + }; + } + + private void queuePendingTask(Task task) + { + lock (tasks) tasks.Add(task); + task.ContinueWith(_ => + { + lock (tasks) + tasks.Remove(task); + }); + } + + #region Disposal + + ~RavenLogger() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool isDisposed; + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposed) + return; + + isDisposed = true; + lock (tasks) Task.WaitAll(tasks.ToArray(), 5000); + } + + #endregion + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1fed7f46bc..3e16e90d06 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -15,12 +15,13 @@ - - + + - + + \ No newline at end of file diff --git a/osu.TestProject.props b/osu.TestProject.props index a73a4f8ce2..58de6ec030 100644 --- a/osu.TestProject.props +++ b/osu.TestProject.props @@ -11,7 +11,7 @@ - +