diff --git a/osu-framework b/osu-framework index 0773d895d9..fac688633b 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 0773d895d9aa0729995cd4a23efc28238e35ceed +Subproject commit fac688633b8fcf34ae5d0514c26b03e217161eb4 diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index bd0cc209b6..5b34e46247 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -5,8 +5,6 @@ using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.MathUtils; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; @@ -14,7 +12,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Catch.Tests { - public class CatchBeatmapConversionTest : BeatmapConversionTest + internal class CatchBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; @@ -47,10 +45,10 @@ namespace osu.Game.Rulesets.Catch.Tests } } - protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new CatchBeatmapConverter(); + protected override Ruleset CreateRuleset() => new CatchRuleset(); } - public struct ConvertValue : IEquatable + internal struct ConvertValue : IEquatable { /// /// A sane value to account for osu!stable using ints everwhere. diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs index bce20520d3..097750d7e0 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override Beatmap CreateBeatmap(Ruleset ruleset) + protected override IBeatmap CreateBeatmap(Ruleset ruleset) { var beatmap = new Beatmap { diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs index d13a6bb860..b5cf0e3d1d 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override Beatmap CreateBeatmap(Ruleset ruleset) + protected override IBeatmap CreateBeatmap(Ruleset ruleset) { var beatmap = new Beatmap { diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs index 2b58fcc93c..8a90b48180 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override Beatmap CreateBeatmap(Ruleset ruleset) + protected override IBeatmap CreateBeatmap(Ruleset ruleset) { var beatmap = new Beatmap { diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseHyperdash.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseHyperdash.cs index e7f936ca2a..896582bf0a 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseHyperdash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseHyperdash.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override Beatmap CreateBeatmap(Ruleset ruleset) + protected override IBeatmap CreateBeatmap(Ruleset ruleset) { var beatmap = new Beatmap { BeatmapInfo = { Ruleset = ruleset.RulesetInfo } }; diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs new file mode 100644 index 0000000000..5b4af6ea8a --- /dev/null +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -0,0 +1,43 @@ +// 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.Linq; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Rulesets.Catch.Objects; + +namespace osu.Game.Rulesets.Catch.Beatmaps +{ + public class CatchBeatmap : Beatmap + { + public override IEnumerable GetStatistics() + { + int fruits = HitObjects.Count(s => s is Fruit); + int juiceStreams = HitObjects.Count(s => s is JuiceStream); + int bananaShowers = HitObjects.Count(s => s is BananaShower); + + return new[] + { + new BeatmapStatistic + { + Name = @"Fruit Count", + Content = fruits.ToString(), + Icon = FontAwesome.fa_circle_o + }, + new BeatmapStatistic + { + Name = @"Juice Stream Count", + Content = juiceStreams.ToString(), + Icon = FontAwesome.fa_circle + }, + new BeatmapStatistic + { + Name = @"Banana Shower Count", + Content = bananaShowers.ToString(), + Icon = FontAwesome.fa_circle + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 34e5f425fd..f40ef67dfb 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -13,9 +13,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { public class CatchBeatmapConverter : BeatmapConverter { + public CatchBeatmapConverter(IBeatmap beatmap) + : base(beatmap) + { + } + protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; - protected override IEnumerable ConvertHitObject(HitObject obj, Beatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap) { var curveData = obj as IHasCurve; var positionData = obj as IHasXPosition; @@ -64,5 +69,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps X = positionData.X / CatchPlayfield.BASE_WIDTH }; } + + protected override Beatmap CreateBeatmap() => new CatchBeatmap(); } } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index dfd10e0df7..e16f5fcb60 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -12,16 +12,21 @@ using OpenTK; namespace osu.Game.Rulesets.Catch.Beatmaps { - public class CatchBeatmapProcessor : BeatmapProcessor + public class CatchBeatmapProcessor : BeatmapProcessor { - public override void PostProcess(Beatmap beatmap) + public CatchBeatmapProcessor(IBeatmap beatmap) + : base(beatmap) { - initialiseHyperDash(beatmap.HitObjects); + } - base.PostProcess(beatmap); + public override void PostProcess() + { + initialiseHyperDash((List)Beatmap.HitObjects); + + base.PostProcess(); int index = 0; - foreach (var obj in beatmap.HitObjects) + foreach (var obj in Beatmap.HitObjects.OfType()) obj.IndexInBeatmap = index++; } diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index cfe0fc5cec..2325a8cad9 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -13,12 +13,17 @@ using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Difficulty; +using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets.Catch { public class CatchRuleset : Ruleset { - public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new CatchRulesetContainer(this, beatmap, isForCurrentRuleset); + public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new CatchRulesetContainer(this, beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap); + public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap); public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { @@ -138,7 +143,7 @@ namespace osu.Game.Rulesets.Catch public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap); + public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap); public override int? LegacyID => 2; diff --git a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs similarity index 55% rename from osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs rename to osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 876b394da0..f8351b7519 100644 --- a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -1,21 +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.Beatmaps; -using osu.Game.Rulesets.Catch.Beatmaps; -using osu.Game.Rulesets.Catch.Objects; using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; -namespace osu.Game.Rulesets.Catch +namespace osu.Game.Rulesets.Catch.Difficulty { - public class CatchDifficultyCalculator : DifficultyCalculator + public class CatchDifficultyCalculator : DifficultyCalculator { - public CatchDifficultyCalculator(Beatmap beatmap) : base(beatmap) + public CatchDifficultyCalculator(IBeatmap beatmap) : base(beatmap) { } public override double Calculate(Dictionary categoryDifficulty = null) => 0; - - protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter(); } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs index df7578799f..8e19c0614a 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs @@ -6,10 +6,11 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; using System; +using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModHardRock : ModHardRock, IApplicableToHitObject + public class CatchModHardRock : ModHardRock, IApplicableToHitObject { public override double ScoreMultiplier => 1.12; public override bool Ranked => true; @@ -17,9 +18,11 @@ namespace osu.Game.Rulesets.Catch.Mods private float lastStartX; private int lastStartTime; - public void ApplyToHitObject(CatchHitObject hitObject) + public void ApplyToHitObject(HitObject hitObject) { - float position = hitObject.X; + var catchObject = (CatchHitObject)hitObject; + + float position = catchObject.X; int startTime = (int)hitObject.StartTime; if (lastStartX == 0) @@ -60,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.Mods position += rand; } - hitObject.X = position; + catchObject.X = position; return; } @@ -79,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Mods } } - hitObject.X = position; + catchObject.X = position; lastStartX = position; lastStartTime = startTime; diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 95ffd41518..548813fbd2 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Catch.Objects Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5; } + + protected override HitWindows CreateHitWindows() => null; } public enum FruitVisualRepresentation diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index d63d1bd331..d5c5eb844a 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Replays Dashing = dashing; } - public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap) { Position = legacyFrame.Position.X / CatchPlayfield.BASE_WIDTH; Dashing = legacyFrame.ButtonState == ReplayButtonState.Left1; diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 022a8a8b43..070dc19a6f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -4,7 +4,6 @@ using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; -using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; using osu.Game.Rulesets.Catch.Replays; @@ -20,8 +19,8 @@ namespace osu.Game.Rulesets.Catch.UI { public class CatchRulesetContainer : ScrollingRulesetContainer { - public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(ruleset, beatmap, isForCurrentRuleset) + public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { } @@ -29,10 +28,6 @@ namespace osu.Game.Rulesets.Catch.UI protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); - protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); - - protected override BeatmapConverter CreateBeatmapConverter() => new CatchBeatmapConverter(); - protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, GetVisualRepresentation); public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index 81c537e53c..5ae899f6d6 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -5,8 +5,6 @@ using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.MathUtils; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -14,17 +12,14 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests { - public class ManiaBeatmapConversionTest : BeatmapConversionTest + internal class ManiaBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; - private bool isForCurrentRuleset; - [NonParallelizable] - [TestCase("basic", false)] - public void Test(string name, bool isForCurrentRuleset) + [TestCase("basic")] + public new void Test(string name) { - this.isForCurrentRuleset = isForCurrentRuleset; base.Test(name); } @@ -38,10 +33,10 @@ namespace osu.Game.Rulesets.Mania.Tests }; } - protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new ManiaBeatmapConverter(isForCurrentRuleset, beatmap); + protected override Ruleset CreateRuleset() => new ManiaRuleset(); } - public struct ConvertValue : IEquatable + internal struct ConvertValue : IEquatable { /// /// A sane value to account for osu!stable using ints everwhere. diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaHitObjects.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseManiaHitObjects.cs index 281c2789af..a4109722d4 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaHitObjects.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseManiaHitObjects.cs @@ -4,6 +4,8 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Tests.Visual; @@ -17,6 +19,14 @@ namespace osu.Game.Rulesets.Mania.Tests { public TestCaseManiaHitObjects() { + Note note1 = new Note(); + Note note2 = new Note(); + HoldNote holdNote = new HoldNote { StartTime = 1000 }; + + note1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + note2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + Add(new FillFlowContainer { Anchor = Anchor.Centre, @@ -43,14 +53,14 @@ namespace osu.Game.Rulesets.Mania.Tests RelativeChildSize = new Vector2(1, 10000), Children = new[] { - new DrawableNote(new Note(), ManiaAction.Key1) + new DrawableNote(note1, ManiaAction.Key1) { Y = 5000, LifetimeStart = double.MinValue, LifetimeEnd = double.MaxValue, AccentColour = Color4.Red }, - new DrawableNote(new Note(), ManiaAction.Key1) + new DrawableNote(note2, ManiaAction.Key1) { Y = 6000, LifetimeStart = double.MinValue, @@ -77,13 +87,13 @@ namespace osu.Game.Rulesets.Mania.Tests RelativeChildSize = new Vector2(1, 10000), Children = new[] { - new DrawableHoldNote(new HoldNote { Duration = 1000 } , ManiaAction.Key1) + new DrawableHoldNote(holdNote, ManiaAction.Key1) { Y = 5000, Height = 1000, LifetimeStart = double.MinValue, LifetimeEnd = double.MaxValue, - AccentColour = Color4.Red + AccentColour = Color4.Red, } } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs index 053f478027..dff2b2d56a 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs @@ -8,6 +8,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; @@ -83,13 +85,16 @@ namespace osu.Game.Rulesets.Mania.Tests int col = rng.Next(0, 4); - var note = new DrawableNote(new Note { Column = col }, ManiaAction.Key1) + var note = new Note { Column = col }; + note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + var drawableNote = new DrawableNote(note, ManiaAction.Key1) { AccentColour = playfield.Columns.ElementAt(col).AccentColour }; - playfield.OnJudgement(note, new ManiaJudgement { Result = HitResult.Perfect }); - playfield.Columns[col].OnJudgement(note, new ManiaJudgement { Result = HitResult.Perfect }); + playfield.OnJudgement(drawableNote, new ManiaJudgement { Result = HitResult.Perfect }); + playfield.Columns[col].OnJudgement(drawableNote, new ManiaJudgement { Result = HitResult.Perfect }); }); } @@ -162,32 +167,24 @@ namespace osu.Game.Rulesets.Mania.Tests for (double t = start_time; t <= start_time + duration; t += 100) { - playfield.Add(new DrawableNote(new Note - { - StartTime = t, - Column = 0 - }, ManiaAction.Key1)); + var note1 = new Note { StartTime = t, Column = 0 }; + var note2 = new Note { StartTime = t, Column = 3 }; - playfield.Add(new DrawableNote(new Note - { - StartTime = t, - Column = 3 - }, ManiaAction.Key4)); + note1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + note2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + playfield.Add(new DrawableNote(note1, ManiaAction.Key1)); + playfield.Add(new DrawableNote(note2, ManiaAction.Key4)); } - playfield.Add(new DrawableHoldNote(new HoldNote - { - StartTime = start_time, - Duration = duration, - Column = 1 - }, ManiaAction.Key2)); + var holdNote1 = new HoldNote { StartTime = start_time, Duration = duration, Column = 1 }; + var holdNote2 = new HoldNote { StartTime = start_time, Duration = duration, Column = 2 }; - playfield.Add(new DrawableHoldNote(new HoldNote - { - StartTime = start_time, - Duration = duration, - Column = 2 - }, ManiaAction.Key3)); + holdNote1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + holdNote2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + playfield.Add(new DrawableHoldNote(holdNote1, ManiaAction.Key2)); + playfield.Add(new DrawableHoldNote(holdNote2, ManiaAction.Key3)); } } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index 6af3719f83..ad5f8e447d 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; @@ -29,5 +30,27 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { Stages.Add(defaultStage); } + + public override IEnumerable GetStatistics() + { + int notes = HitObjects.Count(s => s is Note); + int holdnotes = HitObjects.Count(s => s is HoldNote); + + return new[] + { + new BeatmapStatistic + { + Name = @"Note Count", + Content = notes.ToString(), + Icon = FontAwesome.fa_circle_o + }, + new BeatmapStatistic + { + Name = @"Hold Note Count", + Content = holdnotes.ToString(), + Icon = FontAwesome.fa_circle + }, + }; + } } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 60b92cb7b3..19fef9eb54 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -33,18 +33,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps private ManiaBeatmap beatmap; - public ManiaBeatmapConverter(bool isForCurrentRuleset, Beatmap original) + public ManiaBeatmapConverter(IBeatmap beatmap) + : base(beatmap) { - IsForCurrentRuleset = isForCurrentRuleset; + IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo); - var roundedCircleSize = Math.Round(original.BeatmapInfo.BaseDifficulty.CircleSize); - var roundedOverallDifficulty = Math.Round(original.BeatmapInfo.BaseDifficulty.OverallDifficulty); + var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); + var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); - if (isForCurrentRuleset) + if (IsForCurrentRuleset) TargetColumns = (int)Math.Max(1, roundedCircleSize); else { - float percentSliderOrSpinner = (float)original.HitObjects.Count(h => h is IHasEndTime) / original.HitObjects.Count; + float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count(); if (percentSliderOrSpinner < 0.2) TargetColumns = 7; else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5) @@ -56,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } } - protected override Beatmap ConvertBeatmap(Beatmap original) + protected override Beatmap ConvertBeatmap(IBeatmap original) { BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; @@ -68,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override Beatmap CreateBeatmap() => beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }); - protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) { var maniaOriginal = original as ManiaHitObject; if (maniaOriginal != null) @@ -112,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The original hit object. /// The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap. /// The hit objects generated. - private IEnumerable generateSpecific(HitObject original, Beatmap originalBeatmap) + private IEnumerable generateSpecific(HitObject original, IBeatmap originalBeatmap) { var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern, originalBeatmap); @@ -128,7 +129,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The original hit object. /// The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap. /// The hit objects generated. - private IEnumerable generateConverted(HitObject original, Beatmap originalBeatmap) + private IEnumerable generateConverted(HitObject original, IBeatmap originalBeatmap) { var endTimeData = original as IHasEndTime; var distanceData = original as IHasDistance; @@ -165,7 +166,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator { - public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap) + public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 3b5c028bfd..afa9bdbbd7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private PatternType convertType; - public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap) + public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { convertType = PatternType.None; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 743e230cb2..3f34afee85 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { private readonly double endTime; - public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Beatmap originalBeatmap) + public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, new Pattern(), originalBeatmap) { var endtimeData = HitObject as IHasEndTime; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 652c92dd78..cec3e18ad6 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private readonly PatternType convertType; - public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair, Beatmap originalBeatmap) + public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime)); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index 02306846a3..930597c1ad 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// The beatmap which is being converted from. /// - protected readonly Beatmap OriginalBeatmap; + protected readonly IBeatmap OriginalBeatmap; - protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap) + protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) : base(hitObject, beatmap, previousPattern) { if (random == null) throw new ArgumentNullException(nameof(random)); @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy drainTime /= 1000; BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; - conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; + conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + OriginalBeatmap.HitObjects.Count() / drainTime * 9f) / 38f * 5f / 1.15; conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); return conversionDifficulty.Value; diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs similarity index 89% rename from osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs rename to osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 5eea346836..2517839355 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -1,16 +1,18 @@ // 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.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; -using System; -using System.Collections.Generic; -namespace osu.Game.Rulesets.Mania +namespace osu.Game.Rulesets.Mania.Difficulty { - internal class ManiaDifficultyCalculator : DifficultyCalculator + internal class ManiaDifficultyCalculator : DifficultyCalculator { private const double star_scaling_factor = 0.018; @@ -31,12 +33,12 @@ namespace osu.Game.Rulesets.Mania /// private readonly List difficultyHitObjects = new List(); - public ManiaDifficultyCalculator(Beatmap beatmap) + public ManiaDifficultyCalculator(IBeatmap beatmap) : base(beatmap) { } - public ManiaDifficultyCalculator(Beatmap beatmap, Mod[] mods) + public ManiaDifficultyCalculator(IBeatmap beatmap, Mod[] mods) : base(beatmap, mods) { } @@ -48,18 +50,17 @@ namespace osu.Game.Rulesets.Mania int columnCount = (Beatmap as ManiaBeatmap)?.TotalColumns ?? 7; - foreach (var hitObject in Beatmap.HitObjects) - difficultyHitObjects.Add(new ManiaHitObjectDifficulty(hitObject, columnCount)); - // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. - difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); + // Note: Stable sort is done so that the ordering of hitobjects with equal start times doesn't change + difficultyHitObjects.AddRange(Beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime)); if (!calculateStrainValues()) return 0; double starRating = calculateDifficulty() * star_scaling_factor; - categoryDifficulty?.Add("Strain", starRating); + if (categoryDifficulty != null) + categoryDifficulty["Strain"] = starRating; return starRating; } @@ -140,7 +141,5 @@ namespace osu.Game.Rulesets.Mania return difficulty; } - - protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, beatmap); } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs new file mode 100644 index 0000000000..e6e3028d62 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -0,0 +1,126 @@ +// 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.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Difficulty +{ + public class ManiaPerformanceCalculator : PerformanceCalculator + { + private Mod[] mods; + + // Score after being scaled by non-difficulty-increasing mods + private double scaledScore; + + private int countPerfect; + private int countGreat; + private int countGood; + private int countOk; + private int countMeh; + private int countMiss; + + public ManiaPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) + : base(ruleset, beatmap, score) + { + } + + public override double Calculate(Dictionary categoryDifficulty = null) + { + mods = Score.Mods; + scaledScore = Score.TotalScore; + countPerfect = Convert.ToInt32(Score.Statistics[HitResult.Perfect]); + countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]); + countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]); + countOk = Convert.ToInt32(Score.Statistics[HitResult.Ok]); + countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); + + if (mods.Any(m => !m.Ranked)) + return 0; + + IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); + + double scoreMultiplier = 1.0; + foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) + scoreMultiplier *= m.ScoreMultiplier; + + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / scoreMultiplier; + + // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. + // The specific number has no intrinsic meaning and can be adjusted as needed. + double multiplier = 0.8; + + if (mods.Any(m => m is ModNoFail)) + multiplier *= 0.9; + if (mods.Any(m => m is ModEasy)) + multiplier *= 0.5; + + double strainValue = computeStrainValue(); + double accValue = computeAccuracyValue(strainValue); + double totalValue = + Math.Pow( + Math.Pow(strainValue, 1.1) + + Math.Pow(accValue, 1.1), 1.0 / 1.1 + ) * multiplier; + + if (categoryDifficulty != null) + { + categoryDifficulty["Strain"] = strainValue; + categoryDifficulty["Accuracy"] = accValue; + } + + return totalValue; + } + + private double computeStrainValue() + { + // Obtain strain difficulty + double strainValue = Math.Pow(5 * Math.Max(1, Attributes["Strain"] / 0.2) - 4.0, 2.2) / 135.0; + + // Longer maps are worth more + strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); + + if (scaledScore <= 500000) + strainValue = 0; + else if (scaledScore <= 600000) + strainValue *= (scaledScore - 500000) / 100000 * 0.3; + else if (scaledScore <= 700000) + strainValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25; + else if (scaledScore <= 800000) + strainValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20; + else if (scaledScore <= 900000) + strainValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15; + else + strainValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1; + + return strainValue; + } + + private double computeAccuracyValue(double strainValue) + { + double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + if (hitWindowGreat <= 0) + return 0; + + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution + double accuracyValue = Math.Max(0.0, 0.2 - (hitWindowGreat - 34) * 0.006667) + * strainValue + * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); + + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer + // accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + + return accuracyValue; + } + + private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss; + } +} diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgement.cs new file mode 100644 index 0000000000..9630ba9273 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteJudgement.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.Scoring; + +namespace osu.Game.Rulesets.Mania.Judgements +{ + public class HoldNoteJudgement : ManiaJudgement + { + public override bool AffectsCombo => false; + protected override int NumericResultFor(HitResult result) => 0; + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0546cbc174..02ecb3afda 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -15,12 +15,18 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Difficulty; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania { public class ManiaRuleset : Ruleset { - public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new ManiaRulesetContainer(this, beatmap, isForCurrentRuleset); + public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new ManiaRulesetContainer(this, beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap); + public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score); public override IEnumerable ConvertLegacyMods(LegacyMods mods) { @@ -182,7 +188,7 @@ namespace osu.Game.Rulesets.Mania public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods); + public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods); public override int? LegacyID => 3; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs index dbd30121a8..e02db68a28 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs @@ -3,19 +3,18 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Mods { - public abstract class ManiaKeyMod : Mod, IApplicableToBeatmapConverter + public abstract class ManiaKeyMod : Mod, IApplicableToBeatmapConverter { public override string ShortenedName => Name; public abstract int KeyCount { get; } public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier public override bool Ranked => true; - public void ApplyToBeatmapConverter(BeatmapConverter beatmapConverter) + public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter) { var mbc = (ManiaBeatmapConverter)beatmapConverter; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs index 197b37b3f5..7f3985b26d 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs @@ -11,19 +11,23 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToRulesetContainer + public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToRulesetContainer { public override string Name => "Dual Stages"; public override string ShortenedName => "DS"; public override string Description => @"Double the stages, double the fun!"; public override double ScoreMultiplier => 0; - public void ApplyToBeatmapConverter(BeatmapConverter beatmapConverter) + private bool isForCurrentRuleset; + + public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter) { var mbc = (ManiaBeatmapConverter)beatmapConverter; + isForCurrentRuleset = mbc.IsForCurrentRuleset; + // Although this can work, for now let's not allow keymods for mania-specific beatmaps - if (mbc.IsForCurrentRuleset) + if (isForCurrentRuleset) return; mbc.TargetColumns *= 2; @@ -34,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Mods var mrc = (ManiaRulesetContainer)rulesetContainer; // Although this can work, for now let's not allow keymods for mania-specific beatmaps - if (mrc.IsForCurrentRuleset) + if (isForCurrentRuleset) return; var newDefinitions = new List(); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index f8b2311a13..8791e8ed86 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -99,6 +99,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void UpdateState(ArmedState state) { + switch (state) + { + case ArmedState.Hit: + // Good enough for now, we just want them to have a lifetime end + this.Delay(2000).Expire(); + break; + } + } + + protected override void CheckForJudgements(bool userTriggered, double timeOffset) + { + if (tail.AllJudged) + AddJudgement(new HoldNoteJudgement { Result = HitResult.Perfect }); } protected override void Update() @@ -191,6 +204,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// private class DrawableTailNote : DrawableNote { + /// + /// Lenience of release hit windows. This is to make cases where the hold note release + /// is timed alongside presses of other hit objects less awkward. + /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps + /// + private const double release_window_lenience = 1.5; + private readonly DrawableHoldNote holdNote; public DrawableTailNote(DrawableHoldNote holdNote, ManiaAction action) @@ -203,6 +223,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void CheckForJudgements(bool userTriggered, double timeOffset) { + // Factor in the release lenience + timeOffset /= release_window_lenience; + if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 4cf22ccd39..22fa93a308 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// The tail note of the hold. /// - public readonly Note Tail = new TailNote(); + public readonly Note Tail = new Note(); /// /// The time between ticks of this hold. @@ -94,24 +94,5 @@ namespace osu.Game.Rulesets.Mania.Objects }); } } - - /// - /// The tail of the hold note. - /// - private class TailNote : Note - { - /// - /// Lenience of release hit windows. This is to make cases where the hold note release - /// is timed alongside presses of other hit objects less awkward. - /// - private const double release_window_lenience = 1.5; - - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - HitWindows *= release_window_lenience; - } - } } } diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index 4f0e02ff0d..e183098a51 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.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.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Objects.Types; using osu.Game.Rulesets.Objects; @@ -12,12 +10,6 @@ namespace osu.Game.Rulesets.Mania.Objects { public virtual int Column { get; set; } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - HitWindows.AllowsPerfect = true; - HitWindows.AllowsOk = true; - } + protected override HitWindows CreateHitWindows() => new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs new file mode 100644 index 0000000000..063b626af1 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs @@ -0,0 +1,36 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Objects +{ + public class ManiaHitWindows : HitWindows + { + private static readonly IReadOnlyDictionary base_ranges = new Dictionary + { + { HitResult.Perfect, (44.8, 38.8, 27.8) }, + { HitResult.Great, (128, 98, 68 ) }, + { HitResult.Good, (194, 164, 134) }, + { HitResult.Ok, (254, 224, 194) }, + { HitResult.Meh, (302, 272, 242) }, + { HitResult.Miss, (376, 346, 316) }, + }; + + public override void SetDifficulty(double difficulty) + { + AllowsPerfect = true; + AllowsOk = true; + + Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]); + Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); + Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); + Ok = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Ok]); + Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]); + Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index 8d86325dd9..bc9fd6e06f 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -24,10 +24,10 @@ namespace osu.Game.Rulesets.Mania.Replays Actions.AddRange(actions); } - public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap) { // We don't need to fully convert, just create the converter - var converter = new ManiaBeatmapConverter(beatmap.BeatmapInfo.RulesetID == 3, beatmap); + var converter = new ManiaBeatmapConverter(beatmap); // NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling // elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage. diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 76afaf270f..7123aab901 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(ruleset, beatmap, isForCurrentRuleset) + public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { // Generate the bar lines double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; @@ -85,8 +85,6 @@ namespace osu.Game.Rulesets.Mania.UI public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant); - protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(IsForCurrentRuleset, WorkingBeatmap.Beatmap); - protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) { ManiaAction action = Playfield.Columns.ElementAt(h.Column).Action; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index 6ac3c016a0..386ae5eb05 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -5,17 +5,15 @@ using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.MathUtils; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Beatmaps; using OpenTK; namespace osu.Game.Rulesets.Osu.Tests { - public class OsuBeatmapConversionTest : BeatmapConversionTest + internal class OsuBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; @@ -42,10 +40,10 @@ namespace osu.Game.Rulesets.Osu.Tests }; } - protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new OsuBeatmapConverter(); + protected override Ruleset CreateRuleset() => new OsuRuleset(); } - public struct ConvertValue : IEquatable + internal struct ConvertValue : IEquatable { /// /// A sane value to account for osu!stable using ints everwhere. diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs index f7f73f74a5..cb1ea5cc5f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs @@ -93,12 +93,36 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Big Single, Large StackOffset", () => testSimpleBigLargeStackOffset()); AddStep("Big 1 Repeat, Large StackOffset", () => testSimpleBigLargeStackOffset(1)); + + AddStep("Distance Overflow", () => testDistanceOverflow()); + AddStep("Distance Overflow 1 Repeat", () => testDistanceOverflow(1)); } private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats); private void testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10); + private void testDistanceOverflow(int repeats = 0) + { + var slider = new Slider + { + StartTime = Time.Current + 1000, + Position = new Vector2(239, 176), + ControlPoints = new List + { + Vector2.Zero, + new Vector2(154, 28), + new Vector2(52, -34) + }, + Distance = 700, + RepeatCount = repeats, + RepeatSamples = createEmptySamples(repeats), + StackHeight = 10 + }; + + addSlider(slider, 2, 2); + } + private void testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats); private void testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats); diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs new file mode 100644 index 0000000000..6d90c2a875 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -0,0 +1,43 @@ +// 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.Linq; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Beatmaps +{ + public class OsuBeatmap : Beatmap + { + public override IEnumerable GetStatistics() + { + int circles = HitObjects.Count(c => c is HitCircle); + int sliders = HitObjects.Count(s => s is Slider); + int spinners = HitObjects.Count(s => s is Spinner); + + return new[] + { + new BeatmapStatistic + { + Name = @"Circle Count", + Content = circles.ToString(), + Icon = FontAwesome.fa_circle_o + }, + new BeatmapStatistic + { + Name = @"Slider Count", + Content = sliders.ToString(), + Icon = FontAwesome.fa_circle + }, + new BeatmapStatistic + { + Name = @"Spinner Count", + Content = spinners.ToString(), + Icon = FontAwesome.fa_circle + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 1236076f48..80eb808f6e 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -14,9 +14,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { internal class OsuBeatmapConverter : BeatmapConverter { + public OsuBeatmapConverter(IBeatmap beatmap) + : base(beatmap) + { + } + protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasPosition) }; - protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) { var curveData = original as IHasCurve; var endTimeData = original as IHasEndTime; @@ -45,8 +50,7 @@ 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 }; } else @@ -60,5 +64,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps }; } } + + protected override Beatmap CreateBeatmap() => new OsuBeatmap(); } } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index afa2437bf6..c7c9f4a01a 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -8,12 +8,17 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Beatmaps { - internal class OsuBeatmapProcessor : BeatmapProcessor + internal class OsuBeatmapProcessor : BeatmapProcessor { - public override void PostProcess(Beatmap beatmap) + public OsuBeatmapProcessor(IBeatmap beatmap) + : base(beatmap) { - applyStacking(beatmap); - base.PostProcess(beatmap); + } + + public override void PostProcess() + { + applyStacking((Beatmap)Beatmap); + base.PostProcess(); } private void applyStacking(Beatmap beatmap) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs similarity index 64% rename from osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs rename to osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 926a7975f3..3ed072a275 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -4,55 +4,54 @@ using System; using System.Collections.Generic; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; +using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; -using osu.Game.Rulesets.Osu.OsuDifficulty.Skills; -namespace osu.Game.Rulesets.Osu.OsuDifficulty +namespace osu.Game.Rulesets.Osu.Difficulty { - public class OsuDifficultyCalculator : DifficultyCalculator + public class OsuDifficultyCalculator : DifficultyCalculator { private const int section_length = 400; private const double difficulty_multiplier = 0.0675; - public OsuDifficultyCalculator(Beatmap beatmap) + public OsuDifficultyCalculator(IBeatmap beatmap) : base(beatmap) { } - public OsuDifficultyCalculator(Beatmap beatmap, Mod[] mods) + public OsuDifficultyCalculator(IBeatmap beatmap, Mod[] mods) : base(beatmap, mods) { } - protected override void PreprocessHitObjects() - { - new OsuBeatmapProcessor().PostProcess(Beatmap); - } - public override double Calculate(Dictionary categoryDifficulty = null) { - OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Beatmap.HitObjects, TimeRate); + OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap((List)Beatmap.HitObjects, TimeRate); Skill[] skills = { new Aim(), new Speed() }; - double sectionEnd = section_length / TimeRate; + double sectionLength = section_length * TimeRate; + + // The first object doesn't generate a strain, so we begin with an incremented section end + double currentSectionEnd = 2 * sectionLength; + foreach (OsuDifficultyHitObject h in beatmap) { - while (h.BaseObject.StartTime > sectionEnd) + while (h.BaseObject.StartTime > currentSectionEnd) { foreach (Skill s in skills) { s.SaveCurrentPeak(); - s.StartNewSectionFrom(sectionEnd); + s.StartNewSectionFrom(currentSectionEnd); } - sectionEnd += section_length; + currentSectionEnd += sectionLength; } foreach (Skill s in skills) @@ -72,7 +71,5 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty return starRating; } - - protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new OsuBeatmapConverter(); } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs similarity index 75% rename from osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs rename to osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 8f0feca207..eeb776fa6e 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,35 +5,46 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Osu.Scoring +namespace osu.Game.Rulesets.Osu.Difficulty { - public class OsuPerformanceCalculator : PerformanceCalculator + public class OsuPerformanceCalculator : PerformanceCalculator { private readonly int countHitCircles; private readonly int beatmapMaxCombo; private Mod[] mods; + + /// + /// Approach rate adjusted by mods. + /// private double realApproachRate; + + /// + /// Overall difficulty adjusted by mods. + /// + private double realOverallDifficulty; + private double accuracy; private int scoreMaxCombo; - private int count300; - private int count100; - private int count50; + private int countGreat; + private int countGood; + private int countMeh; private int countMiss; - public OsuPerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) + public OsuPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) : base(ruleset, beatmap, score) { countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle); - beatmapMaxCombo = Beatmap.HitObjects.Count; - beatmapMaxCombo += Beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count) + 1; + beatmapMaxCombo = Beatmap.HitObjects.Count(); + // Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above) + beatmapMaxCombo += Beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); } public override double Calculate(Dictionary categoryRatings = null) @@ -41,9 +52,9 @@ namespace osu.Game.Rulesets.Osu.Scoring mods = Score.Mods; accuracy = Score.Accuracy; scoreMaxCombo = Score.MaxCombo; - count300 = Convert.ToInt32(Score.Statistics[HitResult.Great]); - count100 = Convert.ToInt32(Score.Statistics[HitResult.Good]); - count50 = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]); + countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]); + countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]); countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); // Don't count scores made with supposedly unranked mods @@ -58,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Scoring ar = Math.Min(10, ar * 1.4); if (mods.Any(m => m is OsuModEasy)) ar = Math.Max(0, ar / 2); - double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450); + + double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / TimeRate; + double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + realApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5; + realOverallDifficulty = (80 - 0.5 - hitWindowGreat) / 6; // Custom multipliers for NoFail and SpunOut. double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things @@ -85,6 +100,9 @@ namespace osu.Game.Rulesets.Osu.Scoring categoryRatings.Add("Aim", aimValue); categoryRatings.Add("Speed", speedValue); categoryRatings.Add("Accuracy", accuracyValue); + categoryRatings.Add("OD", realOverallDifficulty); + categoryRatings.Add("AR", realApproachRate); + categoryRatings.Add("Max Combo", beatmapMaxCombo); } return totalValue; @@ -121,8 +139,9 @@ namespace osu.Game.Rulesets.Osu.Scoring aimValue *= approachRateFactor; + // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. if (mods.Any(h => h is OsuModHidden)) - aimValue *= 1.18f; + aimValue *= 1.02 + (11.0f - realApproachRate) / 50.0; // Gives a 1.04 bonus for AR10, a 1.06 bonus for AR9, a 1.02 bonus for AR11. if (mods.Any(h => h is OsuModFlashlight)) { @@ -133,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Scoring // Scale the aim value with accuracy _slightly_ aimValue *= 0.5f + accuracy / 2.0f; // It is important to also consider accuracy difficulty when doing that - aimValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500; + aimValue *= 0.98f + Math.Pow(realOverallDifficulty, 2) / 2500; return aimValue; } @@ -153,10 +172,13 @@ namespace osu.Game.Rulesets.Osu.Scoring if (beatmapMaxCombo > 0) speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f); + if (mods.Any(m => m is OsuModHidden)) + speedValue *= 1.18f; + // Scale the speed value with accuracy _slightly_ speedValue *= 0.5f + accuracy / 2.0f; // It is important to also consider accuracy difficulty when doing that - speedValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500; + speedValue *= 0.98f + Math.Pow(realOverallDifficulty, 2) / 2500; return speedValue; } @@ -168,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Scoring int amountHitObjectsWithAccuracy = countHitCircles; if (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = ((count300 - (totalHits - amountHitObjectsWithAccuracy)) * 6 + count100 * 2 + count50) / (amountHitObjectsWithAccuracy * 6); + betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (amountHitObjectsWithAccuracy * 6); else betterAccuracyPercentage = 0; @@ -178,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Scoring // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution - double accuracyValue = Math.Pow(1.52163f, Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f; + double accuracyValue = Math.Pow(1.52163f, realOverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f; // Bonus for many hitcircles - it's harder to keep good accuracy up for longer accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f)); @@ -191,9 +213,7 @@ namespace osu.Game.Rulesets.Osu.Scoring return accuracyValue; } - private double totalHits => count300 + count100 + count50 + countMiss; - private double totalSuccessfulHits => count300 + count100 + count50; - - protected override BeatmapConverter CreateBeatmapConverter() => new OsuBeatmapConverter(); + private double totalHits => countGreat + countGood + countMeh + countMiss; + private double totalSuccessfulHits => countGreat + countGood + countMeh; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs new file mode 100644 index 0000000000..4443a0e66b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections; +using System.Collections.Generic; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing +{ + /// + /// An enumerable container wrapping input as + /// which contains extra data required for difficulty calculation. + /// + public class OsuDifficultyBeatmap : IEnumerable + { + private readonly IEnumerator difficultyObjects; + + /// + /// Creates an enumerator, which preprocesses a list of s recieved as input, wrapping them as + /// which contains extra data required for difficulty calculation. + /// + public OsuDifficultyBeatmap(List objects, double timeRate) + { + // Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases. + // This should probably happen before the objects reach the difficulty calculator. + objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime)); + difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate); + } + + /// + /// Returns an enumerator that enumerates all s in the . + /// + public IEnumerator GetEnumerator() => difficultyObjects; + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private IEnumerator createDifficultyObjectEnumerator(List objects, double timeRate) + { + // The first jump is formed by the first two hitobjects of the map. + // If the map has less than two OsuHitObjects, the enumerator will not return anything. + for (int i = 1; i < objects.Count; i++) + yield return new OsuDifficultyHitObject(objects[i], objects[i - 1], timeRate); + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs similarity index 82% rename from osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs rename to osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 415f76ced8..29de23406b 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -3,16 +3,18 @@ using System; using System.Linq; -using OpenTK; using osu.Game.Rulesets.Osu.Objects; +using OpenTK; -namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing +namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing { /// /// A wrapper around extending it with additional data required for difficulty calculation. /// public class OsuDifficultyHitObject { + private const int normalized_radius = 52; + /// /// The this refers to. /// @@ -28,26 +30,19 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing /// public double DeltaTime { get; private set; } - /// - /// Number of milliseconds until the has to be hit. - /// - public double TimeUntilHit { get; set; } - - private const int normalized_radius = 52; - + private readonly OsuHitObject lastObject; private readonly double timeRate; - private readonly OsuHitObject[] t; - /// /// Initializes the object calculating extra data required for difficulty calculation. /// - public OsuDifficultyHitObject(OsuHitObject[] triangle, double timeRate) + public OsuDifficultyHitObject(OsuHitObject currentObject, OsuHitObject lastObject, double timeRate) { + this.lastObject = lastObject; this.timeRate = timeRate; - t = triangle; - BaseObject = t[0]; + BaseObject = currentObject; + setDistances(); setTimingValues(); // Calculate angle here @@ -63,10 +58,10 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing scalingFactor *= 1 + smallCircleBonus; } - Vector2 lastCursorPosition = t[1].StackedPosition; + Vector2 lastCursorPosition = lastObject.StackedPosition; float lastTravelDistance = 0; - var lastSlider = t[1] as Slider; + var lastSlider = lastObject as Slider; if (lastSlider != null) { computeSliderCursorPosition(lastSlider); @@ -80,8 +75,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing private void setTimingValues() { // Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure. - DeltaTime = Math.Max(40, (t[0].StartTime - t[1].StartTime) / timeRate); - TimeUntilHit = 450; // BaseObject.PreEmpt; + DeltaTime = Math.Max(50, (BaseObject.StartTime - lastObject.StartTime) / timeRate); } private void computeSliderCursorPosition(Slider slider) @@ -107,7 +101,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing } }); - var scoringTimes = slider.NestedHitObjects.Select(t => t.StartTime); + // Skip the head circle + var scoringTimes = slider.NestedHitObjects.Skip(1).Select(t => t.StartTime); foreach (var time in scoringTimes) computeVertex(time); computeVertex(slider.EndTime); diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs similarity index 85% rename from osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs rename to osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 5c736d7bb5..0a45c62671 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -2,9 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; +using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; -namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills +namespace osu.Game.Rulesets.Osu.Difficulty.Skills { /// /// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances. diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Skill.cs similarity index 96% rename from osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs rename to osu.Game.Rulesets.Osu/Difficulty/Skills/Skill.cs index 983599432f..47037c1503 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Skill.cs @@ -3,11 +3,11 @@ using System; using System.Collections.Generic; +using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; +using osu.Game.Rulesets.Osu.Difficulty.Utils; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; -using osu.Game.Rulesets.Osu.OsuDifficulty.Utils; -namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills +namespace osu.Game.Rulesets.Osu.Difficulty.Skills { /// /// Used to processes strain values of s, keep track of strain levels caused by the processed objects diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs similarity index 93% rename from osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs rename to osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index ae3caa1e66..b807f20037 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -1,9 +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.Osu.OsuDifficulty.Preprocessing; +using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; -namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills +namespace osu.Game.Rulesets.Osu.Difficulty.Skills { /// /// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit. diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs b/osu.Game.Rulesets.Osu/Difficulty/Utils/History.cs similarity index 98% rename from osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs rename to osu.Game.Rulesets.Osu/Difficulty/Utils/History.cs index f6933a3e5d..55bd950209 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Utils/History.cs @@ -5,7 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; -namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils +namespace osu.Game.Rulesets.Osu.Difficulty.Utils { /// /// An indexed stack with Push() only, which disposes items at the bottom after the capacity is full. diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs index 8d4c342740..ea33ec9ae0 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs @@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuEditRulesetContainer : OsuRulesetContainer { - public OsuEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(ruleset, beatmap, isForCurrentRuleset) + public OsuEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 7bf0651443..dce1fc2851 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Edit { } - protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new OsuEditRulesetContainer(ruleset, beatmap, true); + protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new OsuEditRulesetContainer(ruleset, beatmap); protected override IReadOnlyList CompositionTools => new ICompositionTool[] { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index cf71116d47..7a30e6b134 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -5,20 +5,23 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using OpenTK; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHardRock : ModHardRock, IApplicableToHitObject + public class OsuModHardRock : ModHardRock, IApplicableToHitObject { public override double ScoreMultiplier => 1.06; public override bool Ranked => true; - public void ApplyToHitObject(OsuHitObject hitObject) + public void ApplyToHitObject(HitObject hitObject) { - hitObject.Position = new Vector2(hitObject.Position.X, OsuPlayfield.BASE_SIZE.Y - hitObject.Y); + var osuObject = (OsuHitObject)hitObject; + + osuObject.Position = new Vector2(osuObject.Position.X, OsuPlayfield.BASE_SIZE.Y - osuObject.Y); var slider = hitObject as Slider; if (slider == null) diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 2b7b7783e2..54126b934f 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -71,5 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects } public virtual void OffsetPosition(Vector2 offset) => Position += offset; + + protected override HitWindows CreateHitWindows() => new OsuHitWindows(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs new file mode 100644 index 0000000000..8405498554 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs @@ -0,0 +1,29 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class OsuHitWindows : HitWindows + { + private static readonly IReadOnlyDictionary base_ranges = new Dictionary + { + { HitResult.Great, (160, 100, 40) }, + { HitResult.Good, (280, 200, 120) }, + { HitResult.Meh, (400, 300, 200) }, + { HitResult.Miss, (400, 400, 400) }, + }; + + public override void SetDifficulty(double difficulty) + { + Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); + Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); + Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]); + Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]); + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs deleted file mode 100644 index 5c8ab0f3d4..0000000000 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections; -using System.Collections.Generic; -using osu.Game.Rulesets.Osu.Objects; - -namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing -{ - /// - /// An enumerable container wrapping input as - /// which contains extra data required for difficulty calculation. - /// - public class OsuDifficultyBeatmap : IEnumerable - { - private readonly IEnumerator difficultyObjects; - private readonly Queue onScreen = new Queue(); - - /// - /// Creates an enumerator, which preprocesses a list of s recieved as input, wrapping them as - /// which contains extra data required for difficulty calculation. - /// - public OsuDifficultyBeatmap(List objects, double timeRate) - { - // Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases. - // This should probably happen before the objects reach the difficulty calculator. - objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime)); - difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate); - } - - /// - /// Returns an enumerator that enumerates all s in the . - /// The inner loop adds objects that appear on screen into a queue until we need to hit the next object. - /// The outer loop returns objects from this queue one at a time, only after they had to be hit, and should no longer be on screen. - /// This means that we can loop through every object that is on screen at the time when a new one appears, - /// allowing us to determine a reading strain for the object that just appeared. - /// - public IEnumerator GetEnumerator() - { - while (true) - { - // Add upcoming objects to the queue until we have at least one object that had been hit and can be dequeued. - // This means there is always at least one object in the queue unless we reached the end of the map. - do - { - if (!difficultyObjects.MoveNext()) - break; // New objects can't be added anymore, but we still need to dequeue and return the ones already on screen. - - OsuDifficultyHitObject latest = difficultyObjects.Current; - // Calculate flow values here - - foreach (OsuDifficultyHitObject h in onScreen) - { - // ReSharper disable once PossibleNullReferenceException (resharper not smart enough to understand IEnumerator.MoveNext()) - h.TimeUntilHit -= latest.DeltaTime; - // Calculate reading strain here - } - - onScreen.Enqueue(latest); - } - while (onScreen.Peek().TimeUntilHit > 0); // Keep adding new objects on screen while there is still time before we have to hit the next one. - - if (onScreen.Count == 0) break; // We have reached the end of the map and enumerated all the objects. - yield return onScreen.Dequeue(); // Remove and return objects one by one that had to be hit before the latest one appeared. - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private IEnumerator createDifficultyObjectEnumerator(List objects, double timeRate) - { - // We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object. - OsuHitObject[] triangle = new OsuHitObject[3]; - - // OsuDifficultyHitObject construction requires three components, an extra copy of the first OsuHitObject is used at the beginning. - if (objects.Count > 1) - { - triangle[1] = objects[0]; // This copy will get shifted to the last spot in the triangle. - triangle[0] = objects[0]; // This component corresponds to the real first OsuHitOject. - } - - // The final component of the first triangle will be the second OsuHitOject of the map, which forms the first jump. - // If the map has less than two OsuHitObjects, the enumerator will not return anything. - for (int i = 1; i < objects.Count; ++i) - { - triangle[2] = triangle[1]; - triangle[1] = triangle[0]; - triangle[0] = objects[i]; - - yield return new OsuDifficultyHitObject(triangle, timeRate); - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index e0ecee97a3..c455bb2af6 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -5,29 +5,29 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Osu.OsuDifficulty; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using System.Collections.Generic; -using System.Linq; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Difficulty; namespace osu.Game.Rulesets.Osu { public class OsuRuleset : Ruleset { - public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new OsuRulesetContainer(this, beatmap, isForCurrentRuleset); + public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new OsuRulesetContainer(this, beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap); + public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap); public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { @@ -37,36 +37,6 @@ namespace osu.Game.Rulesets.Osu new KeyBinding(InputKey.MouseRight, OsuAction.RightButton), }; - public override IEnumerable GetBeatmapStatistics(WorkingBeatmap beatmap) - { - IEnumerable hitObjects = beatmap.Beatmap.HitObjects; - IEnumerable circles = hitObjects.Where(c => !(c is IHasEndTime)); - IEnumerable sliders = hitObjects.Where(s => s is IHasCurve); - IEnumerable spinners = hitObjects.Where(s => s is IHasEndTime && !(s is IHasCurve)); - - return new[] - { - new BeatmapStatistic - { - Name = @"Circle Count", - Content = circles.Count().ToString(), - Icon = FontAwesome.fa_circle_o - }, - new BeatmapStatistic - { - Name = @"Slider Count", - Content = sliders.Count().ToString(), - Icon = FontAwesome.fa_circle - }, - new BeatmapStatistic - { - Name = @"Spinner Count", - Content = spinners.Count().ToString(), - Icon = FontAwesome.fa_circle - } - }; - } - public override IEnumerable ConvertLegacyMods(LegacyMods mods) { if (mods.HasFlag(LegacyMods.Nightcore)) @@ -181,9 +151,9 @@ namespace osu.Game.Rulesets.Osu public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods); + public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods); - public override PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score); public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs index 6f2512cc33..4412b6efab 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Replays Actions.AddRange(actions); } - public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap) { Position = legacyFrame.Position; if (legacyFrame.MouseLeft) Actions.Add(OsuAction.LeftButton); diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index 22c7b719cd..ad1052f86a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -7,7 +7,6 @@ using OpenTK; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Replays; @@ -21,17 +20,13 @@ namespace osu.Game.Rulesets.Osu.UI { public class OsuRulesetContainer : RulesetContainer { - public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(ruleset, beatmap, isForCurrentRuleset) + public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { } public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(this); - protected override BeatmapConverter CreateBeatmapConverter() => new OsuBeatmapConverter(); - - protected override BeatmapProcessor CreateBeatmapProcessor() => new OsuBeatmapProcessor(); - protected override Playfield CreatePlayfield() => new OsuPlayfield(); public override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index aa61f2d60b..11586e340b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -5,27 +5,22 @@ using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.MathUtils; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Taiko.Tests { - public class TaikoBeatmapConversionTest : BeatmapConversionTest + internal class TaikoBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - private bool isForCurrentRuleset; - [NonParallelizable] - [TestCase("basic", false), Ignore("See: https://github.com/ppy/osu/issues/2152")] - [TestCase("slider-generating-drumroll", false)] - public void Test(string name, bool isForCurrentRuleset) + [TestCase("basic")] + [TestCase("slider-generating-drumroll")] + public new void Test(string name) { - this.isForCurrentRuleset = isForCurrentRuleset; base.Test(name); } @@ -43,10 +38,10 @@ namespace osu.Game.Rulesets.Taiko.Tests }; } - protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new TaikoBeatmapConverter(isForCurrentRuleset); + protected override Ruleset CreateRuleset() => new TaikoRuleset(); } - public struct ConvertValue : IEquatable + internal struct ConvertValue : IEquatable { /// /// A sane value to account for osu!stable using ints everwhere. diff --git a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs index aa7318b863..1bf24a46bc 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private Container playfieldContainer; [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) + private void load() { AddStep("Hit!", () => addHitJudgement(false)); AddStep("Kiai hit", () => addHitJudgement(true)); @@ -73,6 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Title = @"Sample Beatmap", AuthorString = @"peppy", }, + Ruleset = new TaikoRuleset().RulesetInfo }, ControlPointInfo = controlPointInfo }); @@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Tests RelativeSizeAxes = Axes.X, Height = 768, Clock = new FramedClock(rateAdjustClock), - Children = new[] { rulesetContainer = new TaikoRulesetContainer(rulesets.GetRuleset(1).CreateInstance(), beatmap, true) } + Children = new[] { rulesetContainer = new TaikoRulesetContainer(new TaikoRuleset(), beatmap) } }); } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs new file mode 100644 index 0000000000..36f6df7869 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -0,0 +1,43 @@ +// 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.Linq; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Beatmaps +{ + public class TaikoBeatmap : Beatmap + { + public override IEnumerable GetStatistics() + { + int hits = HitObjects.Count(s => s is Hit); + int drumrolls = HitObjects.Count(s => s is DrumRoll); + int swells = HitObjects.Count(s => s is Swell); + + return new[] + { + new BeatmapStatistic + { + Name = @"Hit Count", + Content = hits.ToString(), + Icon = FontAwesome.fa_circle_o + }, + new BeatmapStatistic + { + Name = @"Drumroll Count", + Content = drumrolls.ToString(), + Icon = FontAwesome.fa_circle + }, + new BeatmapStatistic + { + Name = @"Swell Count", + Content = swells.ToString(), + Icon = FontAwesome.fa_circle + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 2f175a9922..41972b5d20 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Taiko.Objects; using System; using System.Collections.Generic; using System.Linq; -using osu.Game.IO.Serialization; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; @@ -42,16 +41,18 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(HitObject) }; - public TaikoBeatmapConverter(bool isForCurrentRuleset) + public TaikoBeatmapConverter(IBeatmap beatmap) + : base(beatmap) { - this.isForCurrentRuleset = isForCurrentRuleset; + isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(new TaikoRuleset().RulesetInfo); } - protected override Beatmap ConvertBeatmap(Beatmap original) + protected override Beatmap ConvertBeatmap(IBeatmap original) { // Rewrite the beatmap info to add the slider velocity multiplier - BeatmapInfo info = original.BeatmapInfo.DeepClone(); - info.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier; + original.BeatmapInfo = original.BeatmapInfo.Clone(); + original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone(); + original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier; Beatmap converted = base.ConvertBeatmap(original); @@ -70,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps return converted; } - protected override IEnumerable ConvertHitObject(HitObject obj, Beatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap) { var distanceData = obj as IHasDistance; var repeatsData = obj as IHasRepeats; @@ -97,12 +98,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps double distance = distanceData.Distance * spans * legacy_velocity_multiplier; // The velocity of the taiko hit object - calculated as the velocity of a drum roll - double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; + double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength; // The duration of the taiko hit object double taikoDuration = distance / taikoVelocity; // The velocity of the osu! hit object - calculated as the velocity of a slider - double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; + double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; @@ -140,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = j, Samples = currentSamples, - IsStrong = strong, + IsStrong = strong }; } @@ -155,7 +156,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Samples = obj.Samples, IsStrong = strong, Duration = taikoDuration, - TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4, + TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4 }; } } @@ -169,7 +170,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Samples = obj.Samples, IsStrong = strong, Duration = endTimeData.Duration, - RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier), + RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier) }; } else @@ -182,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - IsStrong = strong, + IsStrong = strong }; } else @@ -191,10 +192,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - IsStrong = strong, + IsStrong = strong }; } } } + + protected override Beatmap CreateBeatmap() => new TaikoBeatmap(); } } diff --git a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs similarity index 91% rename from osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs rename to osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 58661d7881..57e1e65064 100644 --- a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -1,15 +1,16 @@ // 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.Taiko.Beatmaps; -using osu.Game.Rulesets.Taiko.Objects; -using System.Collections.Generic; using System; +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Objects; -namespace osu.Game.Rulesets.Taiko +namespace osu.Game.Rulesets.Taiko.Difficulty { - internal class TaikoDifficultyCalculator : DifficultyCalculator + internal class TaikoDifficultyCalculator : DifficultyCalculator { private const double star_scaling_factor = 0.04125; @@ -30,18 +31,23 @@ namespace osu.Game.Rulesets.Taiko /// private readonly List difficultyHitObjects = new List(); - public TaikoDifficultyCalculator(Beatmap beatmap) + public TaikoDifficultyCalculator(IBeatmap beatmap) : base(beatmap) { } + public TaikoDifficultyCalculator(IBeatmap beatmap, Mod[] mods) + : base(beatmap, mods) + { + } + public override double Calculate(Dictionary categoryDifficulty = null) { // Fill our custom DifficultyHitObject class, that carries additional information difficultyHitObjects.Clear(); foreach (var hitObject in Beatmap.HitObjects) - difficultyHitObjects.Add(new TaikoHitObjectDifficulty(hitObject)); + difficultyHitObjects.Add(new TaikoHitObjectDifficulty((TaikoHitObject)hitObject)); // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); @@ -51,10 +57,7 @@ namespace osu.Game.Rulesets.Taiko double starRating = calculateDifficulty() * star_scaling_factor; if (categoryDifficulty != null) - { - categoryDifficulty.Add("Strain", starRating); - categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate); - } + categoryDifficulty["Strain"] = starRating; return starRating; } @@ -132,7 +135,5 @@ namespace osu.Game.Rulesets.Taiko return difficulty; } - - protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new TaikoBeatmapConverter(true); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs new file mode 100644 index 0000000000..9c9cd1f0fb --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -0,0 +1,111 @@ +// 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.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty +{ + public class TaikoPerformanceCalculator : PerformanceCalculator + { + private readonly int beatmapMaxCombo; + + private Mod[] mods; + private int countGreat; + private int countGood; + private int countMeh; + private int countMiss; + + public TaikoPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) + : base(ruleset, beatmap, score) + { + beatmapMaxCombo = beatmap.HitObjects.Count(h => h is Hit); + } + + public override double Calculate(Dictionary categoryDifficulty = null) + { + mods = Score.Mods; + countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]); + countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]); + countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); + + // Don't count scores made with supposedly unranked mods + if (mods.Any(m => !m.Ranked)) + return 0; + + // Custom multipliers for NoFail and SpunOut. + double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things + + if (mods.Any(m => m is ModNoFail)) + multiplier *= 0.90; + + if (mods.Any(m => m is ModHidden)) + multiplier *= 1.10; + + double strainValue = computeStrainValue(); + double accuracyValue = computeAccuracyValue(); + double totalValue = + Math.Pow( + Math.Pow(strainValue, 1.1) + + Math.Pow(accuracyValue, 1.1), 1.0 / 1.1 + ) * multiplier; + + if (categoryDifficulty != null) + { + categoryDifficulty["Strain"] = strainValue; + categoryDifficulty["Accuracy"] = accuracyValue; + } + + return totalValue; + } + + private double computeStrainValue() + { + double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes["Strain"] / 0.0075) - 4.0, 2.0) / 100000.0; + + // Longer maps are worth more + double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0); + strainValue *= lengthBonus; + + // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + strainValue *= Math.Pow(0.985, countMiss); + + // Combo scaling + if (beatmapMaxCombo > 0) + strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(beatmapMaxCombo, 0.5), 1.0); + + if (mods.Any(m => m is ModHidden)) + strainValue *= 1.025; + + if (mods.Any(m => m is ModFlashlight)) + // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. + strainValue *= 1.05 * lengthBonus; + + // Scale the speed value with accuracy _slightly_ + return strainValue * Score.Accuracy; + } + + private double computeAccuracyValue() + { + double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + if (hitWindowGreat <= 0) + return 0; + + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution + double accValue = Math.Pow(150.0 / hitWindowGreat, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0; + + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer + return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + } + + private int totalHits => countGreat + countGood + countMeh + countMiss; + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 64219c7b52..4c9ec5473b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Objects /// private double tickSpacing = 100; + private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -47,9 +49,7 @@ namespace osu.Game.Rulesets.Taiko.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); tickSpacing = timingPoint.BeatLength / TickRate; - - RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * difficulty.OverallDifficulty); - RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * difficulty.OverallDifficulty); + overallDifficulty = difficulty.OverallDifficulty; } protected override void CreateNestedHitObjects() @@ -57,6 +57,9 @@ namespace osu.Game.Rulesets.Taiko.Objects 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); } private void createTicks() diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 63de096238..ffbbe28f2e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -27,5 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// Strong hit objects give more points for hitting the hit object with both keys. /// public bool IsStrong; + + protected override HitWindows CreateHitWindows() => new TaikoHitWindows(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs new file mode 100644 index 0000000000..289f084a45 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs @@ -0,0 +1,29 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public class TaikoHitWindows : HitWindows + { + private static readonly IReadOnlyDictionary base_ranges = new Dictionary + { + { HitResult.Great, (100, 70, 40) }, + { HitResult.Good, (240, 160, 100) }, + { HitResult.Meh, (270, 190, 140) }, + { HitResult.Miss, (400, 400, 400) }, + }; + + public override void SetDifficulty(double difficulty) + { + Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); + Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]); + Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]); + Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs index e510b34ad7..2177a3cbdc 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Replays Actions.AddRange(actions); } - public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap) { if (legacyFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim); if (legacyFrame.MouseRight2) Actions.Add(TaikoAction.RightRim); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 06a8e44a14..abaa8db597 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -13,12 +13,17 @@ using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Difficulty; namespace osu.Game.Rulesets.Taiko { public class TaikoRuleset : Ruleset { - public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new TaikoRulesetContainer(this, beatmap, isForCurrentRuleset); + public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new TaikoRulesetContainer(this, beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap); public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] { @@ -140,7 +145,9 @@ namespace osu.Game.Rulesets.Taiko public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap); + public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap, mods); + + public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new TaikoPerformanceCalculator(this, beatmap, score); public override int? LegacyID => 1; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 3d3c6ab2f3..313c205981 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Scoring; @@ -24,8 +23,8 @@ namespace osu.Game.Rulesets.Taiko.UI { public class TaikoRulesetContainer : ScrollingRulesetContainer { - public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(ruleset, beatmap, isForCurrentRuleset) + public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { } @@ -93,8 +92,6 @@ namespace osu.Game.Rulesets.Taiko.UI public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this); - protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(IsForCurrentRuleset); - public override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo) diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 6e0cf6be2e..489c38c420 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -9,6 +9,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO.Serialization; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Tests.Resources; using OpenTK; @@ -117,7 +118,7 @@ namespace osu.Game.Tests.Beatmaps.Formats public void TestParity(string beatmap) { var legacy = decode(beatmap, out Beatmap json); - json.ShouldDeepEqual(legacy); + json.WithDeepEqual(legacy).IgnoreProperty(r => r.DeclaringType == typeof(HitWindows)).Assert(); } /// diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 6453cdbd3e..f60caf2397 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -275,13 +275,13 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineBeatmapID == b.OnlineBeatmapID)); Assert.IsTrue(set.Beatmaps.Count > 0); var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap; - Assert.IsTrue(beatmap?.HitObjects.Count > 0); + Assert.IsTrue(beatmap?.HitObjects.Any() == true); beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap; - Assert.IsTrue(beatmap?.HitObjects.Count > 0); + Assert.IsTrue(beatmap?.HitObjects.Any() == true); beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap; - Assert.IsTrue(beatmap?.HitObjects.Count > 0); + Assert.IsTrue(beatmap?.HitObjects.Any() == true); beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap; - Assert.IsTrue(beatmap?.HitObjects.Count > 0); + Assert.IsTrue(beatmap?.HitObjects.Any() == true); } private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs index 886c1120d4..0d3e08154f 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs @@ -12,8 +12,12 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Select; using osu.Game.Tests.Beatmaps; @@ -24,7 +28,7 @@ namespace osu.Game.Tests.Visual { private RulesetStore rulesets; private TestBeatmapInfoWedge infoWedge; - private readonly List beatmaps = new List(); + private readonly List beatmaps = new List(); private readonly Bindable beatmap = new Bindable(); [BackgroundDependencyLoader] @@ -72,13 +76,23 @@ namespace osu.Game.Tests.Visual selectBeatmap(testBeatmap); + testBeatmapLabels(ruleset); + // TODO: adjust cases once more info is shown for other gamemodes switch (ruleset) { - case OsuRuleset osu: - testOsuBeatmap(osu); + case OsuRuleset _: testInfoLabels(5); break; + case TaikoRuleset _: + testInfoLabels(5); + break; + case CatchRuleset _: + testInfoLabels(5); + break; + case ManiaRuleset _: + testInfoLabels(4); + break; default: testInfoLabels(2); break; @@ -88,7 +102,7 @@ namespace osu.Game.Tests.Visual testNullBeatmap(); } - private void testOsuBeatmap(OsuRuleset ruleset) + private void testBeatmapLabels(Ruleset ruleset) { AddAssert("check version", () => infoWedge.Info.VersionLabel.Text == $"{ruleset.ShortName}Version"); AddAssert("check title", () => infoWedge.Info.TitleLabel.Text == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title"); @@ -112,7 +126,7 @@ namespace osu.Game.Tests.Visual AddAssert("check no infolabels", () => !infoWedge.Info.InfoLabelContainer.Children.Any()); } - private void selectBeatmap(Beatmap b) + private void selectBeatmap(IBeatmap b) { BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null; @@ -134,11 +148,11 @@ namespace osu.Game.Tests.Visual }); } - private Beatmap createTestBeatmap(RulesetInfo ruleset) + private IBeatmap createTestBeatmap(RulesetInfo ruleset) { List objects = new List(); for (double i = 0; i < 50000; i += 1000) - objects.Add(new HitObject { StartTime = i }); + objects.Add(new TestHitObject { StartTime = i }); return new Beatmap { @@ -153,7 +167,8 @@ namespace osu.Game.Tests.Visual }, Ruleset = ruleset, StarDifficulty = 6, - Version = $"{ruleset.ShortName}Version" + Version = $"{ruleset.ShortName}Version", + BaseDifficulty = new BeatmapDifficulty() }, HitObjects = objects }; @@ -163,5 +178,12 @@ namespace osu.Game.Tests.Visual { public new BufferedWedgeInfo Info => base.Info; } + + private class TestHitObject : HitObject, IHasPosition + { + public float X { get; } = 0; + public float Y { get; } = 0; + public Vector2 Position { get; } = Vector2.Zero; + } } } diff --git a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs b/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs index a6a6130a8f..442ca1322a 100644 --- a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseBreakOverlay.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 osu.Framework.Timing; using osu.Game.Beatmaps.Timing; using System.Collections.Generic; using NUnit.Framework; @@ -16,8 +15,6 @@ namespace osu.Game.Tests.Visual public TestCaseBreakOverlay() { - Clock = new FramedClock(); - Child = breakOverlay = new BreakOverlay(true); AddStep("2s break", () => startBreak(2000)); diff --git a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs index 25f8ba06c4..8fd8880fd6 100644 --- a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs +++ b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs @@ -6,9 +6,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; -using osu.Game.Screens.Multiplayer; +using osu.Game.Screens.Multi.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual @@ -111,6 +112,7 @@ namespace osu.Game.Tests.Visual } }); + AddStep(@"select", () => first.State = SelectionState.Selected); AddStep(@"change title", () => first.Room.Name.Value = @"I Changed Name"); AddStep(@"change host", () => first.Room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }); AddStep(@"change status", () => first.Room.Status.Value = new RoomStatusPlaying()); @@ -121,6 +123,7 @@ namespace osu.Game.Tests.Visual new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 1254 } } }, new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 123189 } } }, }); + AddStep(@"deselect", () => first.State = SelectionState.NotSelected); } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs index 582ab5ecc9..f037d70493 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs @@ -332,7 +332,7 @@ namespace osu.Game.Tests.Visual private readonly Drawable tracker; - public TimingPointVisualiser(Beatmap beatmap, double length) + public TimingPointVisualiser(IBeatmap beatmap, double length) { this.length = length; diff --git a/osu.Game.Tests/Visual/TestCaseHoldToConfirmOverlay.cs b/osu.Game.Tests/Visual/TestCaseHoldToConfirmOverlay.cs new file mode 100644 index 0000000000..6fa49c4edb --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseHoldToConfirmOverlay.cs @@ -0,0 +1,62 @@ +// 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 osu.Framework.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseHoldToConfirmOverlay : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(ExitConfirmOverlay) }; + + public TestCaseHoldToConfirmOverlay() + { + bool fired = false; + + var firedText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Fired!", + TextSize = 50, + Alpha = 0, + }; + + var overlay = new TestHoldToConfirmOverlay + { + Action = () => + { + fired = true; + firedText.FadeTo(1).Then().FadeOut(1000); + } + }; + + Children = new Drawable[] + { + overlay, + firedText + }; + + AddStep("start confirming", () => overlay.Begin()); + AddStep("abort confirming", () => overlay.Abort()); + + AddAssert("ensure aborted", () => !fired); + + AddStep("start confirming", () => overlay.Begin()); + + AddUntilStep(() => fired, "wait until confirmed"); + } + + private class TestHoldToConfirmOverlay : ExitConfirmOverlay + { + protected override bool AllowMultipleFires => true; + + public void Begin() => BeginConfirm(); + public void Abort() => AbortConfirm(); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMods.cs b/osu.Game.Tests/Visual/TestCaseMods.cs index dad8fb8fed..d3d21509fd 100644 --- a/osu.Game.Tests/Visual/TestCaseMods.cs +++ b/osu.Game.Tests/Visual/TestCaseMods.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 System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -17,6 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.UI; using OpenTK.Graphics; namespace osu.Game.Tests.Visual @@ -24,6 +26,19 @@ namespace osu.Game.Tests.Visual [Description("mod select and icon display")] public class TestCaseMods : OsuTestCase { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ModSelectOverlay), + typeof(ModDisplay), + typeof(ModSection), + typeof(ModIcon), + typeof(ModButton), + typeof(ModButtonEmpty), + typeof(DifficultyReductionSection), + typeof(DifficultyIncreaseSection), + typeof(SpecialSection), + }; + private const string unranked_suffix = " (Unranked)"; private RulesetStore rulesets; @@ -66,7 +81,8 @@ namespace osu.Game.Tests.Visual Ruleset ruleset = rulesetInfo.CreateInstance(); AddStep($"switch to {ruleset.Description}", () => modSelect.Ruleset.Value = rulesetInfo); - switch (ruleset) { + switch (ruleset) + { case OsuRuleset or: testOsuMods(or); break; diff --git a/osu.Game.Tests/Visual/TestCaseMultiHeader.cs b/osu.Game.Tests/Visual/TestCaseMultiHeader.cs new file mode 100644 index 0000000000..af51a6221f --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMultiHeader.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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Screens.Multi; +using osu.Game.Screens.Multi.Screens; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseMultiHeader : OsuTestCase + { + public TestCaseMultiHeader() + { + Lobby lobby; + Children = new Drawable[] + { + lobby = new Lobby + { + Padding = new MarginPadding { Top = Header.HEIGHT }, + }, + new Header(lobby), + }; + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMultiScreen.cs b/osu.Game.Tests/Visual/TestCaseMultiScreen.cs new file mode 100644 index 0000000000..6c22fb020f --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMultiScreen.cs @@ -0,0 +1,21 @@ +// 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.Game.Screens.Multi; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseMultiScreen : OsuTestCase + { + public TestCaseMultiScreen() + { + Multiplayer multi = new Multiplayer(); + + AddStep(@"show", () => Add(multi)); + AddWaitStep(5); + AddStep(@"exit", multi.Exit); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs b/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs index 233c418d4a..123c1fe055 100644 --- a/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs +++ b/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs @@ -4,6 +4,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; +using osu.Framework.Configuration.Tracking; +using osu.Framework.Graphics; using osu.Game.Overlays; namespace osu.Game.Tests.Visual @@ -11,36 +13,82 @@ namespace osu.Game.Tests.Visual [TestFixture] public class TestCaseOnScreenDisplay : OsuTestCase { - private FrameworkConfigManager config; - private Bindable frameSyncMode; - - protected override void LoadComplete() - { - base.LoadComplete(); - - Add(new OnScreenDisplay()); - - frameSyncMode = config.GetBindable(FrameworkSetting.FrameSync); - - FrameSync initial = frameSyncMode.Value; - - AddRepeatStep(@"Change frame limiter", setNextMode, 3); - - AddStep(@"Restore frame limiter", () => frameSyncMode.Value = initial); - } - - private void setNextMode() - { - var nextMode = frameSyncMode.Value + 1; - if (nextMode > FrameSync.Unlimited) - nextMode = FrameSync.VSync; - frameSyncMode.Value = nextMode; - } - [BackgroundDependencyLoader] - private void load(FrameworkConfigManager config) + private void load() { - this.config = config; + var config = new TestConfigManager(); + + var osd = new TestOnScreenDisplay(); + osd.BeginTracking(this, config); + Add(osd); + + AddRepeatStep("Change toggle (no bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingNoKeybind), 2); + AddRepeatStep("Change toggle (with bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingWithKeybind), 2); + AddRepeatStep("Change enum (no bind)", () => config.IncrementEnumSetting(TestConfigSetting.EnumSettingNoKeybind), 3); + AddRepeatStep("Change enum (with bind)", () => config.IncrementEnumSetting(TestConfigSetting.EnumSettingWithKeybind), 3); + } + + private class TestConfigManager : ConfigManager + { + public TestConfigManager() + { + InitialiseDefaults(); + } + + protected override void InitialiseDefaults() + { + Set(TestConfigSetting.ToggleSettingNoKeybind, false); + Set(TestConfigSetting.EnumSettingNoKeybind, EnumSetting.Setting1); + Set(TestConfigSetting.ToggleSettingWithKeybind, false); + Set(TestConfigSetting.EnumSettingWithKeybind, EnumSetting.Setting1); + + base.InitialiseDefaults(); + } + + public void ToggleSetting(TestConfigSetting setting) => Set(setting, !Get(setting)); + + public void IncrementEnumSetting(TestConfigSetting setting) + { + var nextValue = Get(setting) + 1; + if (nextValue > EnumSetting.Setting4) + nextValue = EnumSetting.Setting1; + Set(setting, nextValue); + } + + public override TrackedSettings CreateTrackedSettings() => new TrackedSettings + { + new TrackedSetting(TestConfigSetting.ToggleSettingNoKeybind, b => new SettingDescription(b, "toggle setting with no keybind", b ? "enabled" : "disabled")), + new TrackedSetting(TestConfigSetting.EnumSettingNoKeybind, v => new SettingDescription(v, "enum setting with no keybind", v.ToString())), + new TrackedSetting(TestConfigSetting.ToggleSettingWithKeybind, b => new SettingDescription(b, "toggle setting with keybind", b ? "enabled" : "disabled", "fake keybind")), + new TrackedSetting(TestConfigSetting.EnumSettingWithKeybind, v => new SettingDescription(v, "enum setting with keybind", v.ToString(), "fake keybind")), + }; + + protected override void PerformLoad() + { + } + + protected override bool PerformSave() => false; + } + + private enum TestConfigSetting + { + ToggleSettingNoKeybind, + EnumSettingNoKeybind, + ToggleSettingWithKeybind, + EnumSettingWithKeybind + } + + private enum EnumSetting + { + Setting1, + Setting2, + Setting3, + Setting4 + } + + private class TestOnScreenDisplay : OnScreenDisplay + { + protected override void Display(Drawable toDisplay) => toDisplay.FadeIn().ResizeHeightTo(110); } } } diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs index 6ba671c7fc..5bc16fe420 100644 --- a/osu.Game.Tests/Visual/TestCaseReplay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplay.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual // We create a dummy RulesetContainer just to get the replay - we don't want to use mods here // to simulate setting a replay rather than having the replay already set for us beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); - var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap, beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo)); + var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap); // We have the replay var replay = dummyRulesetContainer.Replay; diff --git a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs index 88059d2dcf..06b9c4a6f9 100644 --- a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs +++ b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; -using osu.Game.Screens.Multiplayer; +using osu.Game.Screens.Multi.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual { base.LoadComplete(); - var room = new Room + Room room = new Room { Name = { Value = @"My Awesome Room" }, Host = { Value = new User { Username = @"flyte", Id = 3103765, Country = new Country { FlagName = @"JP" } } }, @@ -71,9 +71,13 @@ namespace osu.Game.Tests.Visual { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Room = room, + RelativeSizeAxes = Axes.Both, + Width = 0.5f, }); + AddStep(@"set room", () => inspector.Room = room); + AddStep(@"null room", () => inspector.Room = null); + AddStep(@"set room", () => inspector.Room = room); AddStep(@"change title", () => room.Name.Value = @"A Better Room Than The Above"); AddStep(@"change host", () => room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }); AddStep(@"change status", () => room.Status.Value = new RoomStatusPlaying()); @@ -88,7 +92,7 @@ namespace osu.Game.Tests.Visual AddStep(@"change room", () => { - var newRoom = new Room + Room newRoom = new Room { Name = { Value = @"My New, Better Than Ever Room" }, Host = { Value = new User { Username = @"Angelsim", Id = 1777162, Country = new Country { FlagName = @"KR" } } }, diff --git a/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs b/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs new file mode 100644 index 0000000000..7a743655f4 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs @@ -0,0 +1,145 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens; +using OpenTK; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseScreenBreadcrumbControl : OsuTestCase + { + private readonly ScreenBreadcrumbControl breadcrumbs; + private Screen currentScreen, changedScreen; + + public TestCaseScreenBreadcrumbControl() + { + TestScreen startScreen; + OsuSpriteText titleText; + + Children = new Drawable[] + { + currentScreen = startScreen = new TestScreenOne(), + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + breadcrumbs = new ScreenBreadcrumbControl(startScreen) + { + RelativeSizeAxes = Axes.X, + }, + titleText = new OsuSpriteText(), + }, + }, + }; + + breadcrumbs.Current.ValueChanged += s => + { + titleText.Text = $"Changed to {s.ToString()}"; + changedScreen = s; + }; + + breadcrumbs.Current.TriggerChange(); + + assertCurrent(); + pushNext(); + assertCurrent(); + pushNext(); + assertCurrent(); + + AddStep(@"make start current", () => + { + startScreen.MakeCurrent(); + currentScreen = startScreen; + }); + + assertCurrent(); + pushNext(); + AddAssert(@"only 2 items", () => breadcrumbs.Items.Count() == 2); + AddStep(@"exit current", () => changedScreen.Exit()); + AddAssert(@"current screen is first", () => startScreen == changedScreen); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + breadcrumbs.StripColour = colours.Blue; + } + + private void pushNext() => AddStep(@"push next screen", () => currentScreen = ((TestScreen)currentScreen).PushNext()); + private void assertCurrent() => AddAssert(@"changedScreen correct", () => currentScreen == changedScreen); + + private abstract class TestScreen : OsuScreen + { + protected abstract string Title { get; } + protected abstract string NextTitle { get; } + protected abstract TestScreen CreateNextScreen(); + + public override string ToString() => Title; + + public TestScreen PushNext() + { + TestScreen screen = CreateNextScreen(); + Push(screen); + + return screen; + } + + protected TestScreen() + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = Title, + }, + new TriangleButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 100, + Text = $"Push {NextTitle}", + Action = () => PushNext(), + }, + }, + }; + } + } + + private class TestScreenOne : TestScreen + { + protected override string Title => @"Screen One"; + protected override string NextTitle => @"Two"; + protected override TestScreen CreateNextScreen() => new TestScreenTwo(); + } + + private class TestScreenTwo : TestScreen + { + protected override string Title => @"Screen Two"; + protected override string NextTitle => @"One"; + protected override TestScreen CreateNextScreen() => new TestScreenOne(); + } + } +} diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 12a017f68c..9aabb434a3 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -6,7 +6,6 @@ using osu.Game.Rulesets.Objects; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.IO.Serialization; using Newtonsoft.Json; using osu.Game.IO.Serialization.Converters; @@ -15,21 +14,27 @@ namespace osu.Game.Beatmaps /// /// A Beatmap containing converted HitObjects. /// - public class Beatmap : IJsonSerializable + public class Beatmap : IBeatmap where T : HitObject { - public BeatmapInfo BeatmapInfo = new BeatmapInfo(); - public ControlPointInfo ControlPointInfo = new ControlPointInfo(); - public List Breaks = new List(); + public BeatmapInfo BeatmapInfo { get; set; } = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = @"Unknown", + Title = @"Unknown", + AuthorString = @"Unknown Creator", + }, + Version = @"Normal", + BaseDifficulty = new BeatmapDifficulty() + }; [JsonIgnore] public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata; - /// - /// The HitObjects this Beatmap contains. - /// - [JsonConverter(typeof(TypedListConverter))] - public List HitObjects = new List(); + public ControlPointInfo ControlPointInfo { get; set; } = new ControlPointInfo(); + + public List Breaks { get; set; } = new List(); /// /// Total amount of break time in the beatmap. @@ -38,51 +43,22 @@ namespace osu.Game.Beatmaps public double TotalBreakTime => Breaks.Sum(b => b.Duration); /// - /// Constructs a new beatmap. + /// The HitObjects this Beatmap contains. /// - /// The original beatmap to use the parameters of. - public Beatmap(Beatmap original = null) - { - BeatmapInfo = original?.BeatmapInfo.DeepClone() ?? BeatmapInfo; - ControlPointInfo = original?.ControlPointInfo ?? ControlPointInfo; - Breaks = original?.Breaks ?? Breaks; - HitObjects = original?.HitObjects ?? HitObjects; + [JsonConverter(typeof(TypedListConverter))] + public List HitObjects = new List(); - if (original == null && Metadata == null) - { - // we may have no metadata in cases we weren't sourced from the database. - // let's fill it (and other related fields) so we don't need to null-check it in future usages. - BeatmapInfo = new BeatmapInfo - { - Metadata = new BeatmapMetadata - { - Artist = @"Unknown", - Title = @"Unknown", - AuthorString = @"Unknown Creator", - }, - Version = @"Normal", - BaseDifficulty = new BeatmapDifficulty() - }; - } - } + IEnumerable IBeatmap.HitObjects => HitObjects; + + public virtual IEnumerable GetStatistics() => Enumerable.Empty(); + + IBeatmap IBeatmap.Clone() => Clone(); + + public Beatmap Clone() => (Beatmap)MemberwiseClone(); } - /// - /// A Beatmap containing un-converted HitObjects. - /// public class Beatmap : Beatmap { - /// - /// Constructs a new beatmap. - /// - /// The original beatmap to use the parameters of. - public Beatmap(Beatmap original) - : base(original) - { - } - - public Beatmap() - { - } + public new Beatmap Clone() => (Beatmap)base.Clone(); } } diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 153cace187..a1bb70135a 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -22,37 +22,37 @@ namespace osu.Game.Beatmaps remove => ObjectConverted -= value; } - /// - /// Checks if a Beatmap can be converted using this Beatmap Converter. - /// - /// The Beatmap to check. - /// Whether the Beatmap can be converted using this Beatmap Converter. - public bool CanConvert(Beatmap beatmap) => ValidConversionTypes.All(t => beatmap.HitObjects.Any(t.IsInstanceOfType)); + public IBeatmap Beatmap { get; } - /// - /// Converts a Beatmap using this Beatmap Converter. - /// - /// The un-converted Beatmap. - /// The converted Beatmap. - public Beatmap Convert(Beatmap original) + protected BeatmapConverter(IBeatmap beatmap) { - // We always operate on a clone of the original beatmap, to not modify it game-wide - return ConvertBeatmap(new Beatmap(original)); + Beatmap = beatmap; } - void IBeatmapConverter.Convert(Beatmap original) => Convert(original); + /// + /// Whether can be converted by this . + /// + public bool CanConvert => !Beatmap.HitObjects.Any() || ValidConversionTypes.All(t => Beatmap.HitObjects.Any(t.IsInstanceOfType)); + + /// + /// Converts . + /// + /// The converted Beatmap. + public IBeatmap Convert() + { + // We always operate on a clone of the original beatmap, to not modify it game-wide + return ConvertBeatmap(Beatmap.Clone()); + } /// /// Performs the conversion of a Beatmap using this Beatmap Converter. /// /// The un-converted Beatmap. /// The converted Beatmap. - protected virtual Beatmap ConvertBeatmap(Beatmap original) + protected virtual Beatmap ConvertBeatmap(IBeatmap original) { var beatmap = CreateBeatmap(); - // todo: this *must* share logic (or directly use) Beatmap's constructor. - // right now this isn't easily possible due to generic entanglement. beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = original.HitObjects.SelectMany(h => convert(h, original)).ToList(); @@ -67,7 +67,7 @@ namespace osu.Game.Beatmaps /// The hit object to convert. /// The un-converted Beatmap. /// The converted hit object. - private IEnumerable convert(HitObject original, Beatmap beatmap) + private IEnumerable convert(HitObject original, IBeatmap beatmap) { // Check if the hitobject is already the converted type T tObject = original as T; @@ -107,6 +107,6 @@ namespace osu.Game.Beatmaps /// The hit object to convert. /// The un-converted Beatmap. /// The converted hit object. - protected abstract IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap); + protected abstract IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap); } } diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 855e8fe881..508232dbfe 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -32,6 +32,11 @@ namespace osu.Game.Beatmaps public double SliderMultiplier { get; set; } = 1; public double SliderTickRate { get; set; } = 1; + /// + /// Returns a shallow-clone of this . + /// + public BeatmapDifficulty Clone() => (BeatmapDifficulty)MemberwiseClone(); + /// /// Maps a difficulty value [0, 10] to a two-piece linear range of values. /// diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index a1b97afc6c..40d62103a8 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -143,5 +143,10 @@ namespace osu.Game.Beatmaps public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && BeatmapSet.Hash == other.BeatmapSet.Hash && (Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile; + + /// + /// Returns a shallow-clone of this . + /// + public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone(); } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 645e52a6c6..36fde8a319 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -9,7 +9,9 @@ using System.Linq.Expressions; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Extensions; +using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps.Formats; @@ -333,7 +335,7 @@ namespace osu.Game.Beatmaps ms.Position = 0; var decoder = Decoder.GetDecoder(sr); - Beatmap beatmap = decoder.Decode(sr); + IBeatmap beatmap = decoder.Decode(sr); beatmap.BeatmapInfo.Path = name; beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); @@ -341,9 +343,16 @@ namespace osu.Game.Beatmaps RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); - // TODO: this should be done in a better place once we actually need to dynamically update it. beatmap.BeatmapInfo.Ruleset = ruleset; - beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0; + + if (ruleset != null) + { + // TODO: this should be done in a better place once we actually need to dynamically update it. + var converted = new DummyConversionBeatmap(beatmap).GetPlayableBeatmap(ruleset); + beatmap.BeatmapInfo.StarDifficulty = ruleset.CreateInstance().CreateDifficultyCalculator(converted).Calculate(); + } + else + beatmap.BeatmapInfo.StarDifficulty = 0; beatmapInfos.Add(beatmap.BeatmapInfo); } @@ -351,5 +360,23 @@ namespace osu.Game.Beatmaps return beatmapInfos; } + + /// + /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. + /// + private class DummyConversionBeatmap : WorkingBeatmap + { + private readonly IBeatmap beatmap; + + public DummyConversionBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo) + { + this.beatmap = beatmap; + } + + protected override IBeatmap GetBeatmap() => beatmap; + protected override Texture GetBackground() => null; + protected override Track GetTrack() => null; + } } } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 8e09d66c42..71406c6034 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps this.audioManager = audioManager; } - protected override Beatmap GetBeatmap() + protected override IBeatmap GetBeatmap() { try { diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs index 8f5a2a4cab..bf1cd7d4ee 100644 --- a/osu.Game/Beatmaps/BeatmapProcessor.cs +++ b/osu.Game/Beatmaps/BeatmapProcessor.cs @@ -2,30 +2,47 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Beatmaps { + public interface IBeatmapProcessor + { + IBeatmap Beatmap { get; } + + /// + /// Post-processes to add mode-specific components that aren't added during conversion. + /// + /// An example of such a usage is for combo colours. + /// + /// + void PostProcess(); + } + /// /// Processes a post-converted Beatmap. /// /// The type of HitObject contained in the Beatmap. - public class BeatmapProcessor - where TObject : HitObject + public class BeatmapProcessor : IBeatmapProcessor { + public IBeatmap Beatmap { get; } + + public BeatmapProcessor(IBeatmap beatmap) + { + Beatmap = beatmap; + } + /// /// Post-processes a Beatmap to add mode-specific components that aren't added during conversion. /// /// An example of such a usage is for combo colours. /// /// - /// The Beatmap to process. - public virtual void PostProcess(Beatmap beatmap) + public virtual void PostProcess() { IHasComboInformation lastObj = null; - foreach (var obj in beatmap.HitObjects.OfType()) + foreach (var obj in Beatmap.HitObjects.OfType()) { if (obj.NewCombo) { diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs deleted file mode 100644 index 5e2d9afd23..0000000000 --- a/osu.Game/Beatmaps/DifficultyCalculator.cs +++ /dev/null @@ -1,64 +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.Objects; -using System.Collections.Generic; -using osu.Game.Rulesets.Mods; -using osu.Framework.Timing; -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; - -namespace osu.Game.Beatmaps -{ - public abstract class DifficultyCalculator - { - protected double TimeRate = 1; - - public abstract double Calculate(Dictionary categoryDifficulty = null); - } - - public abstract class DifficultyCalculator : DifficultyCalculator where T : HitObject - { - protected readonly Beatmap Beatmap; - protected readonly Mod[] Mods; - - protected DifficultyCalculator(Beatmap beatmap, Mod[] mods = null) - { - Mods = mods ?? new Mod[0]; - - var converter = CreateBeatmapConverter(beatmap); - - foreach (var mod in Mods.OfType>()) - mod.ApplyToBeatmapConverter(converter); - - Beatmap = converter.Convert(beatmap); - - ApplyMods(Mods); - - PreprocessHitObjects(); - } - - protected virtual void ApplyMods(Mod[] mods) - { - var clock = new StopwatchClock(); - mods.OfType().ForEach(m => m.ApplyToClock(clock)); - TimeRate = clock.Rate; - - foreach (var mod in Mods.OfType()) - mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty); - - foreach (var h in Beatmap.HitObjects) - h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty); - - foreach (var mod in mods.OfType>()) - foreach (var obj in Beatmap.HitObjects) - mod.ApplyToHitObject(obj); - } - - protected virtual void PreprocessHitObjects() - { - } - - protected abstract BeatmapConverter CreateBeatmapConverter(Beatmap beatmap); - } -} diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 0424ff84f1..8094abe5ed 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; namespace osu.Game.Beatmaps @@ -39,7 +41,7 @@ namespace osu.Game.Beatmaps this.game = game; } - protected override Beatmap GetBeatmap() => new Beatmap(); + protected override IBeatmap GetBeatmap() => new Beatmap(); protected override Texture GetBackground() => game.Textures.Get(@"Backgrounds/bg4"); @@ -53,12 +55,14 @@ namespace osu.Game.Beatmaps { public override IEnumerable GetModsFor(ModType type) => new Mod[] { }; - public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) + public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) { throw new NotImplementedException(); } - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => null; + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap }; + + public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => null; public override string Description => "dummy"; @@ -68,6 +72,14 @@ namespace osu.Game.Beatmaps : base(rulesetInfo) { } + + private class DummyBeatmapConverter : IBeatmapConverter + { + public event Action> ObjectConverted; + public IBeatmap Beatmap { get; set; } + public bool CanConvert => true; + public IBeatmap Convert() => Beatmap; + } } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 655355913c..2aee419d20 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -54,9 +54,11 @@ namespace osu.Game.Beatmaps.Formats base.ParseStreamInto(stream, beatmap); - // objects may be out of order *only* if a user has manually edited an .osu file. - // unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828). - this.beatmap.HitObjects.Sort((x, y) => x.StartTime.CompareTo(y.StartTime)); + // Objects may be out of order *only* if a user has manually edited an .osu file. + // Unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828). + // OrderBy is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted) + // The parsing order of hitobjects matters in mania difficulty calculation + this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList(); foreach (var hitObject in this.beatmap.HitObjects) hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty); diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs new file mode 100644 index 0000000000..fe20bce98a --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmap.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 System.Collections.Generic; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; +using osu.Game.IO.Serialization; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Beatmaps +{ + public interface IBeatmap : IJsonSerializable + { + /// + /// This beatmap's info. + /// + BeatmapInfo BeatmapInfo { get; set; } + + /// + /// This beatmap's metadata. + /// + BeatmapMetadata Metadata { get; } + + /// + /// The control points in this beatmap. + /// + ControlPointInfo ControlPointInfo { get; } + + /// + /// The breaks in this beatmap. + /// + List Breaks { get; } + + /// + /// Total amount of break time in the beatmap. + /// + double TotalBreakTime { get; } + + /// + /// The hitobjects contained by this beatmap. + /// + IEnumerable HitObjects { get; } + + /// + /// Returns statistics for the contained in this beatmap. + /// + /// + IEnumerable GetStatistics(); + + /// + /// Creates a shallow-clone of this beatmap and returns it. + /// + /// The shallow-cloned beatmap. + IBeatmap Clone(); + } +} diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs index 6c25395a56..00566093b8 100644 --- a/osu.Game/Beatmaps/IBeatmapConverter.cs +++ b/osu.Game/Beatmaps/IBeatmapConverter.cs @@ -16,10 +16,16 @@ namespace osu.Game.Beatmaps /// event Action> ObjectConverted; + IBeatmap Beatmap { get; } + /// - /// Converts a Beatmap using this Beatmap Converter. + /// Whether can be converted by this . /// - /// The un-converted Beatmap. - void Convert(Beatmap beatmap); + bool CanConvert { get; } + + /// + /// Converts . + /// + IBeatmap Convert(); } } diff --git a/osu.Game/Beatmaps/Legacy/LegacyBeatmap.cs b/osu.Game/Beatmaps/Legacy/LegacyBeatmap.cs deleted file mode 100644 index eea82dac6d..0000000000 --- a/osu.Game/Beatmaps/Legacy/LegacyBeatmap.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -namespace osu.Game.Beatmaps.Legacy -{ - /// - /// A type of Beatmap loaded from a legacy .osu beatmap file (version <=15). - /// - public class LegacyBeatmap : Beatmap - { - /// - /// Constructs a new beatmap. - /// - /// The original beatmap to use the parameters of. - internal LegacyBeatmap(Beatmap original = null) - : base(original) - { - HitObjects = original?.HitObjects; - } - } -} diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 4080e34e81..66a6206c16 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -14,6 +14,8 @@ using osu.Framework.IO.File; using System.IO; using osu.Game.IO.Serialization; using System.Diagnostics; +using osu.Game.Rulesets; +using osu.Game.Rulesets.UI; using osu.Game.Skinning; namespace osu.Game.Beatmaps @@ -36,7 +38,7 @@ namespace osu.Game.Beatmaps Mods.ValueChanged += mods => applyRateAdjustments(); - beatmap = new AsyncLazy(populateBeatmap); + beatmap = new AsyncLazy(populateBeatmap); background = new AsyncLazy(populateBackground, b => b == null || !b.IsDisposed); track = new AsyncLazy(populateTrack); waveform = new AsyncLazy(populateWaveform); @@ -45,7 +47,7 @@ namespace osu.Game.Beatmaps } /// - /// Saves the . + /// Saves the . /// public void Save() { @@ -55,7 +57,7 @@ namespace osu.Game.Beatmaps Process.Start(path); } - protected abstract Beatmap GetBeatmap(); + protected abstract IBeatmap GetBeatmap(); protected abstract Texture GetBackground(); protected abstract Track GetTrack(); protected virtual Skin GetSkin() => new DefaultSkin(); @@ -63,12 +65,11 @@ namespace osu.Game.Beatmaps protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; public bool BeatmapLoaded => beatmap.IsResultAvailable; - public Beatmap Beatmap => beatmap.Value.Result; - public async Task GetBeatmapAsync() => await beatmap.Value; + public IBeatmap Beatmap => beatmap.Value.Result; + public async Task GetBeatmapAsync() => await beatmap.Value; + private readonly AsyncLazy beatmap; - private readonly AsyncLazy beatmap; - - private Beatmap populateBeatmap() + private IBeatmap populateBeatmap() { var b = GetBeatmap() ?? new Beatmap(); @@ -78,6 +79,57 @@ namespace osu.Game.Beatmaps return b; } + /// + /// Constructs a playable from using the applicable converters for a specific . + /// + /// The returned is in a playable state - all and s + /// have been applied, and s have been fully constructed. + /// + /// + /// The to create a playable for. + /// The converted . + /// If could not be converted to . + public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset) + { + var rulesetInstance = ruleset.CreateInstance(); + + IBeatmapConverter converter = rulesetInstance.CreateBeatmapConverter(Beatmap); + + // Check if the beatmap can be converted + if (!converter.CanConvert) + throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter})."); + + // Apply conversion mods + foreach (var mod in Mods.Value.OfType()) + mod.ApplyToBeatmapConverter(converter); + + // Convert + IBeatmap converted = converter.Convert(); + + // Apply difficulty mods + if (Mods.Value.Any(m => m is IApplicableToDifficulty)) + { + converted.BeatmapInfo = converted.BeatmapInfo.Clone(); + converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone(); + + foreach (var mod in Mods.Value.OfType()) + mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); + } + + // Post-process + rulesetInstance.CreateBeatmapProcessor(converted)?.PostProcess(); + + // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed + foreach (var obj in converted.HitObjects) + obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); + + foreach (var mod in Mods.Value.OfType()) + foreach (var obj in converted.HitObjects) + mod.ApplyToHitObject(obj); + + return converted; + } + public bool BackgroundLoaded => background.IsResultAvailable; public Texture Background => background.Value.Result; public async Task GetBackgroundAsync() => await background.Value; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 509622c2fe..b3082e49de 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Configuration; +using osu.Framework.Configuration.Tracking; using osu.Framework.Platform; using osu.Game.Overlays; using osu.Game.Screens.Select; @@ -95,6 +96,11 @@ namespace osu.Game.Configuration public OsuConfigManager(Storage storage) : base(storage) { } + + public override TrackedSettings CreateTrackedSettings() => new TrackedSettings + { + new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled")) + }; } public enum OsuSetting diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 2a30e0d032..f657c0cae5 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -44,19 +44,6 @@ namespace osu.Game.Graphics.Containers return base.OnClick(state); } - protected override bool OnDragStart(InputState state) - { - if (!base.ReceiveMouseInputAt(state.Mouse.NativeState.Position)) - { - State = Visibility.Hidden; - return true; - } - - return base.OnDragStart(state); - } - - protected override bool OnDrag(InputState state) => State == Visibility.Hidden; - private void onStateChanged(Visibility visibility) { switch (visibility) diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index 3f59eeca97..f5017de639 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -14,14 +14,18 @@ namespace osu.Game.Graphics.UserInterface public class BreadcrumbControl : OsuTabControl { private const float padding = 10; + private const float item_chevron_size = 10; - protected override TabItem CreateTabItem(T value) => new BreadcrumbTabItem(value); + protected override TabItem CreateTabItem(T value) => new BreadcrumbTabItem(value) + { + AccentColour = AccentColour, + }; - protected override float StripWidth() => base.StripWidth() - (padding + 8); + protected override float StripWidth() => base.StripWidth() - (padding + item_chevron_size); public BreadcrumbControl() { - Height = 26; + Height = 32; TabContainer.Spacing = new Vector2(padding, 0f); Current.ValueChanged += tab => { @@ -47,6 +51,7 @@ namespace osu.Game.Graphics.UserInterface public override bool HandleKeyboardInput => State == Visibility.Visible; public override bool HandleMouseInput => State == Visibility.Visible; + public override bool IsRemovable => true; private Visibility state; @@ -77,13 +82,14 @@ namespace osu.Game.Graphics.UserInterface public BreadcrumbTabItem(T value) : base(value) { - Text.TextSize = 16; - Padding = new MarginPadding { Right = padding + 8 }; //padding + chevron width + Text.TextSize = 18; + Text.Margin = new MarginPadding { Vertical = 8 }; + Padding = new MarginPadding { Right = padding + item_chevron_size }; Add(Chevron = new SpriteIcon { Anchor = Anchor.CentreRight, Origin = Anchor.CentreLeft, - Size = new Vector2(12), + Size = new Vector2(item_chevron_size), Icon = FontAwesome.fa_chevron_right, Margin = new MarginPadding { Left = padding }, Alpha = 0f, diff --git a/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs new file mode 100644 index 0000000000..adcf401546 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ScreenBreadcrumbControl.cs @@ -0,0 +1,54 @@ +// 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.Extensions.IEnumerableExtensions; +using osu.Framework.Screens; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// A which follows the active screen (and allows navigation) in a stack. + /// + public class ScreenBreadcrumbControl : BreadcrumbControl + { + private Screen last; + + public ScreenBreadcrumbControl(Screen initialScreen) + { + Current.ValueChanged += newScreen => + { + if (last != newScreen && !newScreen.IsCurrentScreen) + newScreen.MakeCurrent(); + }; + + onPushed(initialScreen); + } + + private void screenChanged(Screen newScreen) + { + if (newScreen == null) return; + + if (last != null) + { + last.Exited -= screenChanged; + last.ModePushed -= onPushed; + } + + last = newScreen; + + newScreen.Exited += screenChanged; + newScreen.ModePushed += onPushed; + + Current.Value = newScreen; + } + + private void onPushed(Screen screen) + { + Items.ToList().SkipWhile(i => i != Current.Value).Skip(1).ForEach(RemoveItem); + AddItem(screen); + + screenChanged(screen); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/SelectionState.cs b/osu.Game/Graphics/UserInterface/SelectionState.cs new file mode 100644 index 0000000000..079ae343eb --- /dev/null +++ b/osu.Game/Graphics/UserInterface/SelectionState.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Graphics.UserInterface +{ + public enum SelectionState + { + NotSelected, + Selected + } +} diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs index c9727725ef..ce6ff7c82d 100644 --- a/osu.Game/IO/Serialization/IJsonSerializable.cs +++ b/osu.Game/IO/Serialization/IJsonSerializable.cs @@ -18,8 +18,6 @@ namespace osu.Game.IO.Serialization public static void DeserializeInto(this string objString, T target) => JsonConvert.PopulateObject(objString, target, CreateGlobalSettings()); - public static T DeepClone(this T obj) where T : IJsonSerializable => Deserialize(Serialize(obj)); - /// /// Creates the default that should be used for all s. /// diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index dd8f00f6cd..fced2e5d95 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -26,7 +26,8 @@ namespace osu.Game.Input.Bindings { new KeyBinding(InputKey.F8, GlobalAction.ToggleChat), new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial), - new KeyBinding(InputKey.F12,GlobalAction.TakeScreenshot), + new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons), + new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), @@ -36,6 +37,9 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Down, GlobalAction.DecreaseVolume), new KeyBinding(InputKey.MouseWheelDown, GlobalAction.DecreaseVolume), new KeyBinding(InputKey.F4, GlobalAction.ToggleMute), + + new KeyBinding(InputKey.Escape, GlobalAction.Back), + new KeyBinding(InputKey.MouseButton1, GlobalAction.Back) }; public IEnumerable InGameKeyBindings => new[] @@ -76,6 +80,11 @@ namespace osu.Game.Input.Bindings QuickRetry, [Description("Take screenshot")] - TakeScreenshot + TakeScreenshot, + [Description("Toggle gameplay mouse buttons")] + ToggleGameplayMouseButtons, + + [Description("Go back")] + Back } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d443ed36ae..fe5ca4f278 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -466,6 +466,9 @@ namespace osu.Game case GlobalAction.ToggleDirect: direct.ToggleVisibility(); return true; + case GlobalAction.ToggleGameplayMouseButtons: + LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get(OsuSetting.MouseDisableButtons)); + return true; } return false; diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 331dcec4c0..a2542c537f 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -12,10 +12,8 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; -using osu.Framework.MathUtils; using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Graphics; @@ -181,7 +179,7 @@ namespace osu.Game.Overlays { textbox.HoldFocus = false; if (1f - ChatHeight.Value < channel_selection_min_height) - transformChatHeightTo(1f - channel_selection_min_height, 800, Easing.OutQuint); + this.TransformBindableTo(ChatHeight, 1f - channel_selection_min_height, 800, Easing.OutQuint); } else textbox.HoldFocus = true; @@ -533,26 +531,5 @@ namespace osu.Game.Overlays api.Queue(req); } - - private void transformChatHeightTo(double newChatHeight, double duration = 0, Easing easing = Easing.None) - { - this.TransformTo(this.PopulateTransform(new TransformChatHeight(), newChatHeight, duration, easing)); - } - - private class TransformChatHeight : Transform - { - private double valueAt(double time) - { - if (time < StartTime) return StartValue; - if (time >= EndTime) return EndValue; - - return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); - } - - public override string TargetMember => "ChatHeight.Value"; - - protected override void Apply(ChatOverlay d, double time) => d.ChatHeight.Value = valueAt(time); - protected override void ReadIntoStartValue(ChatOverlay d) => StartValue = d.ChatHeight.Value; - } } } diff --git a/osu.Game/Overlays/HoldToConfirmOverlay.cs b/osu.Game/Overlays/HoldToConfirmOverlay.cs new file mode 100644 index 0000000000..a0e4bf1a39 --- /dev/null +++ b/osu.Game/Overlays/HoldToConfirmOverlay.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using OpenTK.Graphics; + +namespace osu.Game.Overlays +{ + /// + /// An overlay which will display a black screen that dims over a period before confirming an exit action. + /// Action is BYO (derived class will need to call and from a user event). + /// + public abstract class HoldToConfirmOverlay : Container + { + public Action Action; + + private Box overlay; + + private const int activate_delay = 400; + private const int fadeout_delay = 200; + + private bool fired; + + /// + /// Whether the overlay should be allowed to return from a fired state. + /// + protected virtual bool AllowMultipleFires => false; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + AlwaysPresent = true; + + Children = new Drawable[] + { + overlay = new Box + { + Alpha = 0, + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + } + }; + } + + protected void BeginConfirm() + { + if (!AllowMultipleFires && fired) return; + overlay.FadeIn(activate_delay * (1 - overlay.Alpha), Easing.Out).OnComplete(_ => + { + Action?.Invoke(); + fired = true; + }); + } + + protected void AbortConfirm() + { + if (!AllowMultipleFires && fired) return; + overlay.FadeOut(fadeout_delay, Easing.Out); + } + } +} diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index e80a469f91..7406a9ec4f 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input; using OpenTK.Graphics; @@ -43,7 +44,7 @@ namespace osu.Game.Overlays.KeyBinding } private OsuSpriteText text; - private OsuSpriteText pressAKey; + private OsuTextFlowContainer pressAKey; private FillFlowContainer buttons; @@ -95,10 +96,11 @@ namespace osu.Game.Overlays.KeyBinding Anchor = Anchor.TopRight, Origin = Anchor.TopRight }, - pressAKey = new OsuSpriteText + pressAKey = new OsuTextFlowContainer { - Text = "Press a key to change binding, DEL to delete, ESC to cancel.", - Y = height, + Text = "Press a key to change binding, Shift+Delete to delete, Escape to cancel.", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Margin = new MarginPadding(padding), Alpha = 0, Colour = colours.YellowDark @@ -204,9 +206,16 @@ namespace osu.Game.Overlays.KeyBinding finalise(); return true; case Key.Delete: - bindTarget.UpdateKeyCombination(InputKey.None); - finalise(); - return true; + { + if (state.Keyboard.ShiftPressed) + { + bindTarget.UpdateKeyCombination(InputKey.None); + finalise(); + return true; + } + + break; + } } bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); @@ -223,6 +232,26 @@ namespace osu.Game.Overlays.KeyBinding return true; } + protected override bool OnJoystickPress(InputState state, Framework.Input.JoystickEventArgs args) + { + if (!HasFocus) + return false; + + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); + finalise(); + + return true; + } + + protected override bool OnJoystickRelease(InputState state, Framework.Input.JoystickEventArgs args) + { + if (!HasFocus) + return base.OnJoystickRelease(state, args); + + finalise(); + return true; + } + private void finalise() { if (bindTarget != null) @@ -241,7 +270,7 @@ namespace osu.Game.Overlays.KeyBinding GetContainingInputManager().ChangeFocus(null); pressAKey.FadeOut(300, Easing.OutQuint); - pressAKey.Padding = new MarginPadding { Bottom = -pressAKey.DrawHeight }; + pressAKey.Padding = new MarginPadding { Top = height, Bottom = -pressAKey.DrawHeight }; } protected override void OnFocus(InputState state) @@ -250,7 +279,7 @@ namespace osu.Game.Overlays.KeyBinding AutoSizeEasing = Easing.OutQuint; pressAKey.FadeIn(300, Easing.OutQuint); - pressAKey.Padding = new MarginPadding(); + pressAKey.Padding = new MarginPadding { Top = height }; updateBindTarget(); base.OnFocus(state); diff --git a/osu.Game/Overlays/MainSettings.cs b/osu.Game/Overlays/MainSettings.cs index 09b5be6a85..99a86f19a1 100644 --- a/osu.Game/Overlays/MainSettings.cs +++ b/osu.Game/Overlays/MainSettings.cs @@ -6,9 +6,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; using osu.Game.Screens.Ranking; @@ -96,7 +98,7 @@ namespace osu.Game.Overlays }); } - private class BackButton : OsuClickableContainer + private class BackButton : OsuClickableContainer, IKeyBindingHandler { private AspectContainer aspect; @@ -146,6 +148,20 @@ namespace osu.Game.Overlays aspect.ScaleTo(1, 1000, Easing.OutElastic); return base.OnMouseUp(state, args); } + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: + TriggerOnClick(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; } } } diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 2a4f243606..f4e0e3db04 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -116,6 +116,7 @@ namespace osu.Game.Overlays.Mods } private Mod mod; + private readonly Container scaleContainer; public Mod Mod { @@ -149,14 +150,26 @@ namespace osu.Game.Overlays.Mods protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - switch (args.Button) + scaleContainer.ScaleTo(0.9f, 800, Easing.Out); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + scaleContainer.ScaleTo(1, 500, Easing.OutElastic); + + // only trigger the event if we are inside the area of the button + if (Contains(ToScreenSpace(state.Mouse.Position - Position))) { - case MouseButton.Left: - SelectNext(1); - break; - case MouseButton.Right: - SelectNext(-1); - break; + switch (args.Button) + { + case MouseButton.Left: + SelectNext(1); + break; + case MouseButton.Right: + SelectNext(-1); + break; + } } return true; @@ -176,7 +189,8 @@ namespace osu.Game.Overlays.Mods start = Mods.Length - 1; for (int i = start; i < Mods.Length && i >= 0; i += direction) - if (SelectAt(i)) return; + if (SelectAt(i)) + return; Deselect(); } @@ -242,8 +256,14 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, Children = new Drawable[] { - iconsContainer = new Container + scaleContainer = new Container { + Child = iconsContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }, RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index d63babf3b6..8c8ff89420 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -4,7 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -16,7 +17,8 @@ namespace osu.Game.Overlays.Music { public class PlaylistList : CompositeDrawable { - public Action OnSelect; + public Action Selected; + public Action OrderChanged; private readonly ItemsScrollContainer items; @@ -25,7 +27,8 @@ namespace osu.Game.Overlays.Music InternalChild = items = new ItemsScrollContainer { RelativeSizeAxes = Axes.Both, - OnSelect = set => OnSelect?.Invoke(set) + Selected = set => Selected?.Invoke(set), + OrderChanged = (s, i) => OrderChanged?.Invoke(s, i) }; } @@ -35,34 +38,20 @@ namespace osu.Game.Overlays.Music set { base.Padding = value; } } - public IEnumerable BeatmapSets - { - get { return items.Sets; } - set { items.Sets = value; } - } - public BeatmapSetInfo FirstVisibleSet => items.FirstVisibleSet; - public BeatmapSetInfo NextSet => items.NextSet; - public BeatmapSetInfo PreviousSet => items.PreviousSet; - - public BeatmapSetInfo SelectedSet - { - get { return items.SelectedSet; } - set { items.SelectedSet = value; } - } - - public void AddBeatmapSet(BeatmapSetInfo beatmapSet) => items.AddBeatmapSet(beatmapSet); - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => items.RemoveBeatmapSet(beatmapSet); public void Filter(string searchTerm) => items.SearchTerm = searchTerm; private class ItemsScrollContainer : OsuScrollContainer { - public Action OnSelect; + public Action Selected; + public Action OrderChanged; private readonly SearchContainer search; private readonly FillFlowContainer items; + private readonly IBindable beatmapBacking = new Bindable(); + public ItemsScrollContainer() { Children = new Drawable[] @@ -83,14 +72,36 @@ namespace osu.Game.Overlays.Music }; } - public IEnumerable Sets + [BackgroundDependencyLoader] + private void load(BeatmapManager beatmaps, OsuGameBase osuGame) { - get { return items.Select(x => x.BeatmapSetInfo).ToList(); } - set - { - items.Clear(); - value.ForEach(AddBeatmapSet); - } + beatmaps.GetAllUsableBeatmapSets().ForEach(addBeatmapSet); + beatmaps.ItemAdded += addBeatmapSet; + beatmaps.ItemRemoved += removeBeatmapSet; + + beatmapBacking.BindTo(osuGame.Beatmap); + beatmapBacking.ValueChanged += _ => updateSelectedSet(); + } + + private void addBeatmapSet(BeatmapSetInfo obj) + { + var newItem = new PlaylistItem(obj) { OnSelect = set => Selected?.Invoke(set) }; + + items.Add(newItem); + items.SetLayoutPosition(newItem, items.Count - 1); + } + + private void removeBeatmapSet(BeatmapSetInfo obj) + { + var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == obj.ID); + if (itemToRemove != null) + items.Remove(itemToRemove); + } + + private void updateSelectedSet() + { + foreach (PlaylistItem s in items.Children) + s.Selected = s.BeatmapSetInfo.ID == beatmapBacking.Value.BeatmapSetInfo.ID; } public string SearchTerm @@ -99,34 +110,7 @@ namespace osu.Game.Overlays.Music set { search.SearchTerm = value; } } - public void AddBeatmapSet(BeatmapSetInfo beatmapSet) - { - var newItem = new PlaylistItem(beatmapSet) { OnSelect = set => OnSelect?.Invoke(set) }; - - items.Add(newItem); - items.SetLayoutPosition(newItem, items.Count); - } - - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) - { - var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == beatmapSet.ID); - if (itemToRemove != null) - items.Remove(itemToRemove); - } - - public BeatmapSetInfo SelectedSet - { - get { return items.FirstOrDefault(i => i.Selected)?.BeatmapSetInfo; } - set - { - foreach (PlaylistItem s in items.Children) - s.Selected = s.BeatmapSetInfo.ID == value?.ID; - } - } - public BeatmapSetInfo FirstVisibleSet => items.FirstOrDefault(i => i.MatchingFilter)?.BeatmapSetInfo; - public BeatmapSetInfo NextSet => (items.SkipWhile(i => !i.Selected).Skip(1).FirstOrDefault() ?? items.FirstOrDefault())?.BeatmapSetInfo; - public BeatmapSetInfo PreviousSet => (items.TakeWhile(i => !i.Selected).LastOrDefault() ?? items.LastOrDefault())?.BeatmapSetInfo; private Vector2 nativeDragPosition; private PlaylistItem draggedItem; @@ -227,6 +211,7 @@ namespace osu.Game.Overlays.Music } items.SetLayoutPosition(draggedItem, dstIndex); + OrderChanged?.Invoke(draggedItem.BeatmapSetInfo, dstIndex); } private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 3496c044fb..76c2222f8b 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -1,7 +1,7 @@ // 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; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -19,18 +19,16 @@ namespace osu.Game.Overlays.Music public class PlaylistOverlay : OverlayContainer { private const float transition_duration = 600; - private const float playlist_height = 510; + public Action OrderChanged; + + private BeatmapManager beatmaps; private FilterControl filter; private PlaylistList list; - private BeatmapManager beatmaps; - private readonly Bindable beatmapBacking = new Bindable(); - public IEnumerable BeatmapSets => list.BeatmapSets; - [BackgroundDependencyLoader] private void load(OsuGameBase game, BeatmapManager beatmaps, OsuColour colours) { @@ -60,7 +58,8 @@ namespace osu.Game.Overlays.Music { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = 95, Bottom = 10, Right = 10 }, - OnSelect = itemSelected, + Selected = itemSelected, + OrderChanged = (s, i) => OrderChanged?.Invoke(s, i) }, filter = new FilterControl { @@ -74,30 +73,16 @@ namespace osu.Game.Overlays.Music }, }; - beatmaps.ItemAdded += handleBeatmapAdded; - beatmaps.ItemRemoved += handleBeatmapRemoved; - - list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); - beatmapBacking.BindTo(game.Beatmap); filter.Search.OnCommit = (sender, newText) => { - var beatmap = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); - if (beatmap != null) playSpecified(beatmap); + BeatmapInfo beatmap = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); + if (beatmap != null) + beatmapBacking.Value = beatmaps.GetWorkingBeatmap(beatmap); }; } - protected override void LoadComplete() - { - base.LoadComplete(); - beatmapBacking.ValueChanged += b => list.SelectedSet = b?.BeatmapSetInfo; - beatmapBacking.TriggerChange(); - } - - private void handleBeatmapAdded(BeatmapSetInfo setInfo) => Schedule(() => list.AddBeatmapSet(setInfo)); - private void handleBeatmapRemoved(BeatmapSetInfo setInfo) => Schedule(() => list.RemoveBeatmapSet(setInfo)); - protected override void PopIn() { filter.Search.HoldFocus = true; @@ -123,49 +108,7 @@ namespace osu.Game.Overlays.Music return; } - playSpecified(set.Beatmaps.First()); - } - - public void PlayPrevious() - { - var playable = list.PreviousSet; - - if (playable != null) - { - playSpecified(playable.Beatmaps.First()); - list.SelectedSet = playable; - } - } - - public void PlayNext() - { - var playable = list.NextSet; - - if (playable != null) - { - playSpecified(playable.Beatmaps.First()); - list.SelectedSet = playable; - } - } - - private void playSpecified(BeatmapInfo info) - { - beatmapBacking.Value = beatmaps.GetWorkingBeatmap(info, beatmapBacking); - - var track = beatmapBacking.Value.Track; - - track.Restart(); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (beatmaps != null) - { - beatmaps.ItemAdded -= handleBeatmapAdded; - beatmaps.ItemRemoved -= handleBeatmapRemoved; - } + beatmapBacking.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index b4021f2808..fb4e278b0c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -50,7 +51,10 @@ namespace osu.Game.Overlays private LocalisationEngine localisation; + private BeatmapManager beatmaps; private readonly Bindable beatmapBacking = new Bindable(); + private List beatmapSets; + private BeatmapSetInfo currentSet; private Container dragContainer; private Container playerContainer; @@ -93,8 +97,9 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(OsuGameBase game, OsuColour colours, LocalisationEngine localisation) + private void load(OsuGameBase game, BeatmapManager beatmaps, OsuColour colours, LocalisationEngine localisation) { + this.beatmaps = beatmaps; this.localisation = localisation; Children = new Drawable[] @@ -111,6 +116,7 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.X, Y = player_height + 10, + OrderChanged = playlistOrderChanged }, playerContainer = new Container { @@ -185,7 +191,7 @@ namespace osu.Game.Overlays { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = next, + Action = () => next(), Icon = FontAwesome.fa_step_forward, }, } @@ -214,11 +220,24 @@ namespace osu.Game.Overlays } }; + beatmapSets = beatmaps.GetAllUsableBeatmapSets(); + beatmaps.ItemAdded += handleBeatmapAdded; + beatmaps.ItemRemoved += handleBeatmapRemoved; + beatmapBacking.BindTo(game.Beatmap); playlist.StateChanged += s => playlistButton.FadeColour(s == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); } + private void playlistOrderChanged(BeatmapSetInfo beatmapSetInfo, int index) + { + beatmapSets.Remove(beatmapSetInfo); + beatmapSets.Insert(index, beatmapSetInfo); + } + + private void handleBeatmapAdded(BeatmapSetInfo obj) => beatmapSets.Add(obj); + private void handleBeatmapRemoved(BeatmapSetInfo obj) => beatmapSets.RemoveAll(s => s.ID == obj.ID); + protected override void LoadComplete() { beatmapBacking.ValueChanged += beatmapChanged; @@ -257,7 +276,7 @@ namespace osu.Game.Overlays playButton.Icon = track.IsRunning ? FontAwesome.fa_pause_circle_o : FontAwesome.fa_play_circle_o; - if (track.HasCompleted && !track.Looping && !beatmapBacking.Disabled && playlist.BeatmapSets.Any()) + if (track.HasCompleted && !track.Looping && !beatmapBacking.Disabled && beatmapSets.Any()) next(); } else @@ -271,7 +290,7 @@ namespace osu.Game.Overlays if (track == null) { if (!beatmapBacking.Disabled) - playlist.PlayNext(); + next(true); return; } @@ -284,13 +303,26 @@ namespace osu.Game.Overlays private void prev() { queuedDirection = TransformDirection.Prev; - playlist.PlayPrevious(); + + var playable = beatmapSets.TakeWhile(i => i.ID != current.BeatmapSetInfo.ID).LastOrDefault() ?? beatmapSets.LastOrDefault(); + if (playable != null) + { + beatmapBacking.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmapBacking); + beatmapBacking.Value.Track.Restart(); + } } - private void next() + private void next(bool instant = false) { - queuedDirection = TransformDirection.Next; - playlist.PlayNext(); + if (!instant) + queuedDirection = TransformDirection.Next; + + var playable = beatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).Skip(1).FirstOrDefault() ?? beatmapSets.FirstOrDefault(); + if (playable != null) + { + beatmapBacking.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmapBacking); + beatmapBacking.Value.Track.Restart(); + } } private WorkingBeatmap current; @@ -314,8 +346,8 @@ namespace osu.Game.Overlays else { //figure out the best direction based on order in playlist. - var last = playlist.BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count(); - var next = beatmap == null ? -1 : playlist.BeatmapSets.TakeWhile(b => b.ID != beatmap.BeatmapSetInfo?.ID).Count(); + var last = beatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count(); + var next = beatmap == null ? -1 : beatmapSets.TakeWhile(b => b.ID != beatmap.BeatmapSetInfo?.ID).Count(); direction = last > next ? TransformDirection.Prev : TransformDirection.Next; } diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs index 3dd088891d..1c80f2e626 100644 --- a/osu.Game/Overlays/OnScreenDisplay.cs +++ b/osu.Game/Overlays/OnScreenDisplay.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using OpenTK; using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; +using osu.Game.Configuration; using osu.Game.Graphics.Sprites; namespace osu.Game.Overlays @@ -30,6 +31,7 @@ namespace osu.Game.Overlays private readonly SpriteText textLine3; private const float height = 110; + private const float height_notext = 98; private const float height_contracted = height * 0.9f; private readonly FillFlowContainer optionLights; @@ -101,12 +103,12 @@ namespace osu.Game.Overlays }, textLine3 = new OsuSpriteText { - Padding = new MarginPadding { Bottom = 15 }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Bottom = 15 }, Font = @"Exo2.0-Bold", TextSize = 12, Alpha = 0.3f, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, }, } } @@ -116,9 +118,10 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(FrameworkConfigManager frameworkConfig) + private void load(FrameworkConfigManager frameworkConfig, OsuConfigManager osuConfig) { BeginTracking(this, frameworkConfig); + BeginTracking(this, osuConfig); } private readonly Dictionary<(object, IConfigManager), TrackedSettings> trackedConfigManagers = new Dictionary<(object, IConfigManager), TrackedSettings>(); @@ -175,13 +178,10 @@ namespace osu.Game.Overlays textLine2.Text = description.Value; textLine3.Text = description.Shortcut.ToUpper(); - box.Animate( - b => b.FadeIn(500, Easing.OutQuint), - b => b.ResizeHeightTo(height, 500, Easing.OutQuint) - ).Then( - b => b.FadeOutFromOne(1500, Easing.InQuint), - b => b.ResizeHeightTo(height_contracted, 1500, Easing.InQuint) - ); + if (string.IsNullOrEmpty(textLine3.Text)) + textLine3.Text = "NO KEY BOUND"; + + Display(box); int optionCount = 0; int selectedOption = -1; @@ -213,6 +213,17 @@ namespace osu.Game.Overlays }); } + protected virtual void Display(Drawable toDisplay) + { + toDisplay.Animate( + b => b.FadeIn(500, Easing.OutQuint), + b => b.ResizeHeightTo(height, 500, Easing.OutQuint) + ).Then( + b => b.FadeOutFromOne(1500, Easing.InQuint), + b => b.ResizeHeightTo(height_contracted, 1500, Easing.InQuint) + ); + } + private class OptionLight : Container { private Color4 glowingColour, idleColour; diff --git a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs index 1da51e4a5a..3078c44844 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs @@ -6,7 +6,9 @@ using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using OpenTK; +using OpenTK.Input; using OpenTK.Graphics; using osu.Framework.Configuration; using osu.Framework.Graphics.Shapes; @@ -22,6 +24,7 @@ namespace osu.Game.Overlays.Toolbar private readonly Drawable modeButtonLine; private ToolbarModeButton activeButton; + private RulesetStore rulesets; private readonly Bindable ruleset = new Bindable(); public ToolbarModeSelector() @@ -67,26 +70,42 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader(true)] private void load(RulesetStore rulesets, OsuGame game) { + this.rulesets = rulesets; foreach (var r in rulesets.AvailableRulesets) { modeButtons.Add(new ToolbarModeButton { Ruleset = r, - Action = delegate - { - ruleset.Value = r; - } + Action = delegate { ruleset.Value = r; } }); } ruleset.ValueChanged += rulesetChanged; ruleset.DisabledChanged += disabledChanged; + if (game != null) ruleset.BindTo(game.Ruleset); else ruleset.Value = rulesets.AvailableRulesets.FirstOrDefault(); } + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + base.OnKeyDown(state, args); + + if (state.Keyboard.ControlPressed && !args.Repeat && args.Key >= Key.Number1 && args.Key <= Key.Number9) + { + int requested = args.Key - Key.Number1; + + RulesetInfo found = rulesets.AvailableRulesets.Skip(requested).FirstOrDefault(); + if (found != null) + ruleset.Value = found; + return true; + } + + return false; + } + public override bool HandleKeyboardInput => !ruleset.Disabled && base.HandleKeyboardInput; public override bool HandleMouseInput => !ruleset.Disabled && base.HandleMouseInput; diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index da63495fec..f922c507f7 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -100,6 +100,8 @@ namespace osu.Game.Overlays public bool Adjust(GlobalAction action) { + if (!IsLoaded) return false; + switch (action) { case GlobalAction.DecreaseVolume: diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs new file mode 100644 index 0000000000..070bc7ddb0 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -0,0 +1,41 @@ +// 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.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Difficulty +{ + public abstract class DifficultyCalculator + { + protected readonly IBeatmap Beatmap; + protected readonly Mod[] Mods; + + protected double TimeRate { get; private set; } = 1; + + protected DifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) + { + Beatmap = beatmap; + Mods = mods ?? new Mod[0]; + + ApplyMods(Mods); + } + + protected virtual void ApplyMods(Mod[] mods) + { + var clock = new StopwatchClock(); + mods.OfType().ForEach(m => m.ApplyToClock(clock)); + TimeRate = clock.Rate; + } + + protected virtual void PreprocessHitObjects() + { + } + + public abstract double Calculate(Dictionary categoryDifficulty = null); + } +} diff --git a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs similarity index 52% rename from osu.Game/Rulesets/Scoring/PerformanceCalculator.cs rename to osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index 0f115fff6b..07d9c80061 100644 --- a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -3,41 +3,44 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Scoring +namespace osu.Game.Rulesets.Difficulty { public abstract class PerformanceCalculator - { - public abstract double Calculate(Dictionary categoryDifficulty = null); - } - - public abstract class PerformanceCalculator : PerformanceCalculator - where TObject : HitObject { private readonly Dictionary attributes = new Dictionary(); protected IDictionary Attributes => attributes; - protected readonly Beatmap Beatmap; + protected readonly Ruleset Ruleset; + protected readonly IBeatmap Beatmap; protected readonly Score Score; - protected PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) + protected double TimeRate { get; private set; } = 1; + + protected PerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) { + Ruleset = ruleset; + Beatmap = beatmap; Score = score; - var converter = CreateBeatmapConverter(); - - foreach (var mod in score.Mods.OfType>()) - mod.ApplyToBeatmapConverter(converter); - - Beatmap = converter.Convert(beatmap); - var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); diffCalc.Calculate(attributes); + + ApplyMods(score.Mods); } - protected abstract BeatmapConverter CreateBeatmapConverter(); + protected virtual void ApplyMods(Mod[] mods) + { + var clock = new StopwatchClock(); + mods.OfType().ForEach(m => m.ApplyToClock(clock)); + TimeRate = clock.Rate; + } + + public abstract double Calculate(Dictionary categoryDifficulty = null); } } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 1820053d3d..5f1b9a6bad 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Edit private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool; - protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true); + protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap); protected abstract IReadOnlyList CompositionTools { get; } diff --git a/osu.Game/Rulesets/Edit/HitObjectMask.cs b/osu.Game/Rulesets/Edit/HitObjectMask.cs index ad7c27ad80..61fb700dd3 100644 --- a/osu.Game/Rulesets/Edit/HitObjectMask.cs +++ b/osu.Game/Rulesets/Edit/HitObjectMask.cs @@ -6,6 +6,7 @@ using osu.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Input; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects.Drawables; using OpenTK; @@ -137,10 +138,4 @@ namespace osu.Game.Rulesets.Edit /// public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; } - - public enum SelectionState - { - NotSelected, - Selected - } } diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs index a03a003810..1b8e62b53c 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmapConverter.cs @@ -10,13 +10,12 @@ namespace osu.Game.Rulesets.Mods /// Interface for a that applies changes to a . /// /// The type of converted . - public interface IApplicableToBeatmapConverter : IApplicableMod - where TObject : HitObject + public interface IApplicableToBeatmapConverter : IApplicableMod { /// /// Applies this to a . /// /// The to apply to. - void ApplyToBeatmapConverter(BeatmapConverter beatmapConverter); + void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter); } } diff --git a/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs index 0fd2e398c8..d6f330d9df 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs @@ -8,13 +8,12 @@ namespace osu.Game.Rulesets.Mods /// /// An interface for s that can be applied to s. /// - public interface IApplicableToHitObject : IApplicableMod - where TObject : HitObject + public interface IApplicableToHitObject : IApplicableMod { /// /// Applies this to a . /// /// The to apply to. - void ApplyToHitObject(TObject hitObject); + void ApplyToHitObject(HitObject hitObject); } } diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 090bc737dd..6379be9bfc 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -19,10 +19,10 @@ namespace osu.Game.Rulesets.Mods public void ApplyToDifficulty(BeatmapDifficulty difficulty) { const float ratio = 1.4f; - difficulty.CircleSize *= 1.3f; // CS uses a custom 1.3 ratio. + difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ratio, 10.0f); - difficulty.DrainRate *= ratio; - difficulty.OverallDifficulty *= ratio; + difficulty.DrainRate = Math.Min(difficulty.DrainRate * ratio, 10.0f); + difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ratio, 10.0f); } } } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 13fa61f536..15c24e2975 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.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 System.Collections.Generic; using Newtonsoft.Json; using osu.Framework.Extensions.IEnumerableExtensions; @@ -51,21 +52,15 @@ namespace osu.Game.Rulesets.Objects private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; - private HitWindows hitWindows; - /// /// The hit windows for this . /// - public HitWindows HitWindows - { - get => hitWindows ?? (hitWindows = new HitWindows(overallDifficulty)); - protected set => hitWindows = value; - } + public HitWindows HitWindows { get; set; } - private readonly SortedList nestedHitObjects = new SortedList((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); + private readonly Lazy> nestedHitObjects = new Lazy>(() => new SortedList((h1, h2) => h1.StartTime.CompareTo(h2.StartTime))); [JsonIgnore] - public IReadOnlyList NestedHitObjects => nestedHitObjects; + public IReadOnlyList NestedHitObjects => nestedHitObjects.Value; /// /// Applies default values to this HitObject. @@ -76,9 +71,19 @@ namespace osu.Game.Rulesets.Objects { ApplyDefaultsToSelf(controlPointInfo, difficulty); - nestedHitObjects.Clear(); + if (nestedHitObjects.IsValueCreated) + nestedHitObjects.Value.Clear(); + CreateNestedHitObjects(); - nestedHitObjects.ForEach(h => h.ApplyDefaults(controlPointInfo, difficulty)); + + if (nestedHitObjects.IsValueCreated) + { + nestedHitObjects.Value.ForEach(h => + { + h.HitWindows = HitWindows; + h.ApplyDefaults(controlPointInfo, difficulty); + }); + } } protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) @@ -89,14 +94,24 @@ namespace osu.Game.Rulesets.Objects Kiai = effectPoint.KiaiMode; SampleControlPoint = samplePoint; - overallDifficulty = difficulty.OverallDifficulty; - hitWindows = null; + if (HitWindows == null) + HitWindows = CreateHitWindows(); + HitWindows?.SetDifficulty(difficulty.OverallDifficulty); } protected virtual void CreateNestedHitObjects() { } - protected void AddNested(HitObject hitObject) => nestedHitObjects.Add(hitObject); + protected void AddNested(HitObject hitObject) => nestedHitObjects.Value.Add(hitObject); + + /// + /// Creates the for this . + /// This can be null to indicate that the has no . + /// + /// This will only be invoked if hasn't been set externally (e.g. from a . + /// + /// + protected virtual HitWindows CreateHitWindows() => new HitWindows(); } } diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index bf0878a408..3717209860 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -63,10 +63,10 @@ namespace osu.Game.Rulesets.Objects public bool AllowsOk; /// - /// Constructs hit windows by fitting a parameter to a 2-part piecewise linear function for each hit window. + /// Sets hit windows with values that correspond to a difficulty parameter. /// /// The parameter. - public HitWindows(double difficulty) + public virtual void SetDifficulty(double difficulty) { Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]); Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]); @@ -135,39 +135,5 @@ namespace osu.Game.Rulesets.Objects /// The time offset. /// Whether the can be hit at any point in the future from this time offset. public bool CanBeHit(double timeOffset) => timeOffset <= HalfWindowFor(HitResult.Meh); - - /// - /// Multiplies all hit windows by a value. - /// - /// The hit windows to multiply. - /// The value to multiply each hit window by. - public static HitWindows operator *(HitWindows windows, double value) - { - windows.Perfect *= value; - windows.Great *= value; - windows.Good *= value; - windows.Ok *= value; - windows.Meh *= value; - windows.Miss *= value; - - return windows; - } - - /// - /// Divides all hit windows by a value. - /// - /// The hit windows to divide. - /// The value to divide each hit window by. - public static HitWindows operator /(HitWindows windows, double value) - { - windows.Perfect /= value; - windows.Great /= value; - windows.Good /= value; - windows.Ok /= value; - windows.Meh /= value; - windows.Miss /= value; - - return windows; - } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs index 0db5a1dff1..ea4e7f6907 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs @@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public float X { get; set; } public bool NewCombo { get; set; } + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs index e3b35e2f8e..86a10fd363 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs @@ -12,5 +12,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public double EndTime { get; set; } public double Duration => EndTime - StartTime; + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs index 32fb197c62..a8d7b23df1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs @@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public float X { get; set; } public bool NewCombo { get; set; } + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs index c9b3046698..5a443c2ac2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs @@ -15,5 +15,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public double Duration => EndTime - StartTime; public float X { 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 f83173f498..f015272b2c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs @@ -18,5 +18,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float Y => Position.Y; public bool NewCombo { get; set; } + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index c6033d482c..ec5a002bbb 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -18,5 +18,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float Y => Position.Y; public bool NewCombo { 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 28aac6862e..0141785f31 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -20,5 +20,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float X => Position.X; public float Y => Position.Y; + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs index 72d18664bf..5e9786c84a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs @@ -11,5 +11,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko internal sealed class ConvertHit : HitObject, IHasCombo { public bool NewCombo { get; set; } + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs index e810e687bd..8a9a0db0a7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs @@ -11,5 +11,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasCombo { public bool NewCombo { get; set; } + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs index 193e50aed6..4c8807a1d3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs @@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko public double EndTime { get; set; } public double Duration => EndTime - StartTime; + + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/SliderCurve.cs b/osu.Game/Rulesets/Objects/SliderCurve.cs index 86fe74f9af..3932d8ed9d 100644 --- a/osu.Game/Rulesets/Objects/SliderCurve.cs +++ b/osu.Game/Rulesets/Objects/SliderCurve.cs @@ -99,11 +99,9 @@ namespace osu.Game.Rulesets.Objects cumulativeLength.Add(l); } - //TODO: Figure out if the following code is needed in some cases. Judging by the map - // "Transform" http://osu.ppy.sh/s/484689 it seems like we should _not_ be doing this. // Lengthen slider curves that are too short compared to what's // in the .osu file. - /*if (l < Length && calculatedPath.Count > 1) + if (l < Distance && calculatedPath.Count > 1) { Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2]; double d = diff.Length; @@ -111,9 +109,9 @@ namespace osu.Game.Rulesets.Objects if (d <= 0) return; - calculatedPath[calculatedPath.Count - 1] += diff * (float)((Length - l) / d); - cumulativeLength[calculatedPath.Count - 1] = Length; - }*/ + calculatedPath[calculatedPath.Count - 1] += diff * (float)((Distance - l) / d); + cumulativeLength[calculatedPath.Count - 1] = Distance; + } } public void Calculate() diff --git a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs index adf35ce078..fdd528f296 100644 --- a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Replays.Types /// /// The to extract values from. /// The beatmap. - void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap); + void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap); } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index cd1d030afe..395eeab419 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets { @@ -22,8 +23,6 @@ namespace osu.Game.Rulesets { public readonly RulesetInfo RulesetInfo; - public virtual IEnumerable GetBeatmapStatistics(WorkingBeatmap beatmap) => new BeatmapStatistic[] { }; - public IEnumerable GetAllMods() => Enum.GetValues(typeof(ModType)).Cast() // Confine all mods of each mod type into a single IEnumerable .SelectMany(GetModsFor) @@ -52,14 +51,17 @@ namespace osu.Game.Rulesets /// Attempt to create a hit renderer for a beatmap /// /// The beatmap to create the hit renderer for. - /// Whether the hit renderer should assume the beatmap is for the current ruleset. /// Unable to successfully load the beatmap to be usable with this ruleset. /// - public abstract RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset); + public abstract RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap); - public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null); + public abstract IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap); - public virtual PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => null; + public virtual IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => null; + + public abstract DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null); + + public virtual PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => null; public virtual HitObjectComposer CreateHitObjectComposer() => null; @@ -114,7 +116,8 @@ namespace osu.Game.Rulesets Name = Description, ShortName = ShortName, InstantiationInfo = GetType().AssemblyQualifiedName, - ID = LegacyID + ID = LegacyID, + Available = true }; } } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 67a9a59d4a..1847b63658 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets try { var assembly = Assembly.LoadFrom(file); - loaded_assemblies[assembly] = assembly.GetTypes().First(t => t.IsSubclassOf(typeof(Ruleset))); + loaded_assemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset))); } catch (Exception) { diff --git a/osu.Game/Rulesets/Scoring/Legacy/DatabasedLegacyScoreParser.cs b/osu.Game/Rulesets/Scoring/Legacy/DatabasedLegacyScoreParser.cs new file mode 100644 index 0000000000..bfb2b7c13b --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/DatabasedLegacyScoreParser.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.Beatmaps; + +namespace osu.Game.Rulesets.Scoring.Legacy +{ + /// + /// A which retrieves the applicable and + /// for the score from the database. + /// + public class DatabasedLegacyScoreParser : LegacyScoreParser + { + private readonly RulesetStore rulesets; + private readonly BeatmapManager beatmaps; + + public DatabasedLegacyScoreParser(RulesetStore rulesets, BeatmapManager beatmaps) + { + this.rulesets = rulesets; + this.beatmaps = beatmaps; + } + + protected override Ruleset GetRuleset(int rulesetId) => rulesets.GetRuleset(rulesetId).CreateInstance(); + protected override WorkingBeatmap GetBeatmap(string md5Hash) => beatmaps.GetWorkingBeatmap(beatmaps.QueryBeatmap(b => b.MD5Hash == md5Hash)); + } +} diff --git a/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs index 239f200e29..a90cd79186 100644 --- a/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs +++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs @@ -14,18 +14,9 @@ using System.Linq; namespace osu.Game.Rulesets.Scoring.Legacy { - public class LegacyScoreParser + public abstract class LegacyScoreParser { - private readonly RulesetStore rulesets; - private readonly BeatmapManager beatmaps; - - public LegacyScoreParser(RulesetStore rulesets, BeatmapManager beatmaps) - { - this.rulesets = rulesets; - this.beatmaps = beatmaps; - } - - private Beatmap currentBeatmap; + private IBeatmap currentBeatmap; private Ruleset currentRuleset; public Score Parse(Stream stream) @@ -34,33 +25,35 @@ namespace osu.Game.Rulesets.Scoring.Legacy using (SerializationReader sr = new SerializationReader(stream)) { - score = new Score { Ruleset = rulesets.GetRuleset(sr.ReadByte()) }; - currentRuleset = score.Ruleset.CreateInstance(); + currentRuleset = GetRuleset(sr.ReadByte()); + score = new Score { Ruleset = currentRuleset.RulesetInfo }; /* score.Pass = true;*/ var version = sr.ReadInt32(); /* score.FileChecksum = */ - var beatmapHash = sr.ReadString(); - score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash); - currentBeatmap = beatmaps.GetWorkingBeatmap(score.Beatmap).Beatmap; + currentBeatmap = GetBeatmap(sr.ReadString()).Beatmap; + score.Beatmap = currentBeatmap.BeatmapInfo; /* score.PlayerName = */ score.User = new User { Username = sr.ReadString() }; /* var localScoreChecksum = */ sr.ReadString(); - /* score.Count300 = */ - sr.ReadUInt16(); - /* score.Count100 = */ - sr.ReadUInt16(); - /* score.Count50 = */ - sr.ReadUInt16(); - /* score.CountGeki = */ - sr.ReadUInt16(); - /* score.CountKatu = */ - sr.ReadUInt16(); - /* score.CountMiss = */ - sr.ReadUInt16(); + + var count300 = sr.ReadUInt16(); + var count100 = sr.ReadUInt16(); + var count50 = sr.ReadUInt16(); + var countGeki = sr.ReadUInt16(); + var countKatu = sr.ReadUInt16(); + var countMiss = sr.ReadUInt16(); + + score.Statistics[HitResult.Great] = count300; + score.Statistics[HitResult.Good] = count100; + score.Statistics[HitResult.Meh] = count50; + score.Statistics[HitResult.Perfect] = countGeki; + score.Statistics[HitResult.Ok] = countKatu; + score.Statistics[HitResult.Miss] = countMiss; + score.TotalScore = sr.ReadInt32(); score.MaxCombo = sr.ReadUInt16(); /* score.Perfect = */ @@ -81,6 +74,34 @@ namespace osu.Game.Rulesets.Scoring.Legacy /*OnlineId =*/ sr.ReadInt32(); + switch (score.Ruleset.ID) + { + case 0: + { + int totalHits = count50 + count100 + count300 + countMiss; + score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + count300 * 300) / (totalHits * 300) : 1; + break; + } + case 1: + { + int totalHits = count50 + count100 + count300 + countMiss; + score.Accuracy = totalHits > 0 ? (double)(count100 * 150 + count300 * 300) / (totalHits * 300) : 1; + break; + } + case 2: + { + int totalHits = count50 + count100 + count300 + countMiss + countKatu; + score.Accuracy = totalHits > 0 ? (double)(count50 + count100 + count300 ) / totalHits : 1; + break; + } + case 3: + { + int totalHits = count50 + count100 + count300 + countMiss + countGeki + countKatu; + score.Accuracy = totalHits > 0 ? (double)(count50 * 50 + count100 * 100 + countKatu * 200 + (count300 + countGeki) * 300) / (totalHits * 300) : 1; + break; + } + } + using (var replayInStream = new MemoryStream(compressedReplay)) { byte[] properties = new byte[5]; @@ -150,5 +171,19 @@ namespace osu.Game.Rulesets.Scoring.Legacy return frame; } + + /// + /// Retrieves the for a specific id. + /// + /// The id. + /// The . + protected abstract Ruleset GetRuleset(int rulesetId); + + /// + /// Retrieves the corresponding to an MD5 hash. + /// + /// The MD5 hash. + /// The . + protected abstract WorkingBeatmap GetBeatmap(string md5Hash); } } diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index 957fd037e0..69d25fcb67 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Scoring public Score ReadReplayFile(string replayFilename) { using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename))) - return new LegacyScoreParser(rulesets, beatmaps).Parse(s); + return new DatabasedLegacyScoreParser(rulesets, beatmaps).Parse(s); } } } diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index d1f1807937..e42e74c245 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -190,11 +190,6 @@ namespace osu.Game.Rulesets.UI /// protected readonly WorkingBeatmap WorkingBeatmap; - /// - /// Whether the specified beatmap is assumed to be specific to the current ruleset. - /// - public readonly bool IsForCurrentRuleset; - public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this); protected override Container Content => content; @@ -206,43 +201,18 @@ namespace osu.Game.Rulesets.UI /// /// The ruleset being repesented. /// The beatmap to create the hit renderer for. - /// Whether to assume the beatmap is for the current ruleset. - protected RulesetContainer(Ruleset ruleset, WorkingBeatmap workingBeatmap, bool isForCurrentRuleset) + protected RulesetContainer(Ruleset ruleset, WorkingBeatmap workingBeatmap) : base(ruleset) { Debug.Assert(workingBeatmap != null, "RulesetContainer initialized with a null beatmap."); WorkingBeatmap = workingBeatmap; - IsForCurrentRuleset = isForCurrentRuleset; // ReSharper disable once PossibleNullReferenceException Mods = workingBeatmap.Mods.Value; RelativeSizeAxes = Axes.Both; - BeatmapConverter converter = CreateBeatmapConverter(); - BeatmapProcessor processor = CreateBeatmapProcessor(); - - // Check if the beatmap can be converted - if (!converter.CanConvert(workingBeatmap.Beatmap)) - throw new BeatmapInvalidForRulesetException($"{nameof(Beatmap)} can not be converted for the current ruleset (converter: {converter})."); - - // Apply conversion adjustments before converting - foreach (var mod in Mods.OfType>()) - mod.ApplyToBeatmapConverter(converter); - - // Convert the beatmap - Beatmap = converter.Convert(workingBeatmap.Beatmap); - - // Apply difficulty adjustments from mods before using Difficulty. - foreach (var mod in Mods.OfType()) - mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty); - - // Post-process the beatmap - processor.PostProcess(Beatmap); - - // Apply defaults - foreach (var h in Beatmap.HitObjects) - h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty); + Beatmap = (Beatmap)workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo); KeyBindingInputManager = CreateInputManager(); KeyBindingInputManager.RelativeSizeAxes = Axes.Both; @@ -277,10 +247,6 @@ namespace osu.Game.Rulesets.UI if (mods == null) return; - foreach (var mod in mods.OfType>()) - foreach (var obj in Beatmap.HitObjects) - mod.ApplyToHitObject(obj); - foreach (var mod in mods.OfType>()) mod.ApplyToRulesetContainer(this); } @@ -324,13 +290,6 @@ namespace osu.Game.Rulesets.UI Playfield.Size = GetAspectAdjustedSize() * PlayfieldArea; } - /// - /// Creates a processor to perform post-processing operations - /// on HitObjects in converted Beatmaps. - /// - /// The Beatmap processor. - protected virtual BeatmapProcessor CreateBeatmapProcessor() => new BeatmapProcessor(); - /// /// Computes the size of the in relative coordinate space after aspect adjustments. /// @@ -344,12 +303,6 @@ namespace osu.Game.Rulesets.UI /// protected virtual Vector2 PlayfieldArea => new Vector2(0.75f); // A sane default - /// - /// Creates a converter to convert Beatmap to a specific mode. - /// - /// The Beatmap converter. - protected abstract BeatmapConverter CreateBeatmapConverter(); - /// /// Creates a DrawableHitObject from a HitObject. /// @@ -377,9 +330,8 @@ namespace osu.Game.Rulesets.UI /// /// The ruleset being repesented. /// The beatmap to create the hit renderer for. - /// Whether to assume the beatmap is for the current ruleset. - protected RulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(ruleset, beatmap, isForCurrentRuleset) + protected RulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 1e7f561aba..6f86d20295 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -4,9 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Framework.Graphics.Transforms; using osu.Framework.Input; -using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Drawables; using OpenTK.Input; @@ -90,10 +88,10 @@ namespace osu.Game.Rulesets.UI.Scrolling switch (args.Key) { case Key.Minus: - transformVisibleTimeRangeTo(VisibleTimeRange + time_span_step, 200, Easing.OutQuint); + this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 200, Easing.OutQuint); break; case Key.Plus: - transformVisibleTimeRangeTo(VisibleTimeRange - time_span_step, 200, Easing.OutQuint); + this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 200, Easing.OutQuint); break; } } @@ -101,27 +99,6 @@ namespace osu.Game.Rulesets.UI.Scrolling return false; } - private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None) - { - this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing)); - } - protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(direction); - - private class TransformVisibleTimeRange : Transform - { - private double valueAt(double time) - { - if (time < StartTime) return StartValue; - if (time >= EndTime) return EndValue; - - return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); - } - - public override string TargetMember => "VisibleTimeRange.Value"; - - protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time); - protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value; - } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs index 79f9eaa9e9..3fc67e4e34 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs @@ -8,7 +8,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Lists; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.IO.Serialization; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Timing; @@ -30,8 +29,8 @@ namespace osu.Game.Rulesets.UI.Scrolling /// protected readonly SortedList DefaultControlPoints = new SortedList(Comparer.Default); - protected ScrollingRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(ruleset, beatmap, isForCurrentRuleset) + protected ScrollingRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { } @@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.UI.Scrolling if (index < 0) return new MultiplierControlPoint(time); - return new MultiplierControlPoint(time, DefaultControlPoints[index].DeepClone()); + return new MultiplierControlPoint(time, DefaultControlPoints[index]); } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index adb749b492..e4eaee76fc 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -27,6 +27,7 @@ namespace osu.Game.Screens.Edit protected override BackgroundScreen CreateBackground() => new BackgroundScreenCustom(@"Backgrounds/bg4"); public override bool ShowOverlaysOnEnter => false; + public override bool AllowBeatmapRulesetChange => false; private Box bottomBackground; private Container screenContainer; diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 1d152361df..555c497d92 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -19,6 +19,8 @@ namespace osu.Game.Screens public override bool ShowOverlaysOnEnter => false; + protected override bool AllowBackButton => false; + public Loader() { ValidForResume = false; diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index f48e1925c5..997002327a 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -6,22 +6,24 @@ using System.Collections.Generic; using System.Linq; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Threading; using osu.Game.Graphics; +using osu.Game.Input.Bindings; using OpenTK; using OpenTK.Graphics; using OpenTK.Input; -using osu.Framework.Audio.Sample; -using osu.Framework.Audio; -using osu.Framework.Configuration; -using osu.Framework.Threading; namespace osu.Game.Screens.Menu { - public class ButtonSystem : Container, IStateful + public class ButtonSystem : Container, IStateful, IKeyBindingHandler { public event Action StateChanged; @@ -146,7 +148,16 @@ namespace osu.Game.Screens.Menu case Key.Space: logo?.TriggerOnClick(state); return true; - case Key.Escape: + } + + return false; + } + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: switch (State) { case MenuState.TopLevel: @@ -155,12 +166,23 @@ namespace osu.Game.Screens.Menu case MenuState.Play: backButton.TriggerOnClick(); return true; + default: + return false; } - + default: return false; } + } - return false; + public bool OnReleased(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: + return true; + default: + return false; + } } private void onPlay() @@ -337,6 +359,7 @@ namespace osu.Game.Screens.Menu logo.ScaleTo(0.5f, 200, Easing.OutQuint); break; } + break; case MenuState.EnteringMode: logoTracking = true; diff --git a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs new file mode 100644 index 0000000000..62605da5a4 --- /dev/null +++ b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Input; +using osu.Game.Overlays; +using OpenTK.Input; + +namespace osu.Game.Screens.Menu +{ + public class ExitConfirmOverlay : HoldToConfirmOverlay + { + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Key == Key.Escape && !args.Repeat) + { + BeginConfirm(); + return true; + } + + return base.OnKeyDown(state, args); + } + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + { + if (args.Key == Key.Escape) + { + AbortConfirm(); + return true; + } + + return base.OnKeyUp(state, args); + } + } +} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index f2ea6d85a8..3b519c5259 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -14,7 +14,7 @@ using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Charts; using osu.Game.Screens.Direct; using osu.Game.Screens.Edit; -using osu.Game.Screens.Multiplayer; +using osu.Game.Screens.Multi; using osu.Game.Screens.Select; using osu.Game.Screens.Tournament; @@ -26,6 +26,8 @@ namespace osu.Game.Screens.Menu public override bool ShowOverlaysOnEnter => buttons.State != MenuState.Initial; + protected override bool AllowBackButton => buttons.State != MenuState.Initial; + private readonly BackgroundScreenDefault background; private Screen songSelect; @@ -39,6 +41,10 @@ namespace osu.Game.Screens.Menu Children = new Drawable[] { + new ExitConfirmOverlay + { + Action = Exit, + }, new ParallaxContainer { ParallaxAmount = 0.01f, @@ -50,7 +56,7 @@ namespace osu.Game.Screens.Menu OnDirect = delegate { Push(new OnlineListing()); }, OnEdit = delegate { Push(new Editor()); }, OnSolo = delegate { Push(consumeSongSelect()); }, - OnMulti = delegate { Push(new Lobby()); }, + OnMulti = delegate { Push(new Multiplayer()); }, OnExit = Exit, } } diff --git a/osu.Game/Screens/Multiplayer/DrawableGameType.cs b/osu.Game/Screens/Multi/Components/DrawableGameType.cs similarity index 96% rename from osu.Game/Screens/Multiplayer/DrawableGameType.cs rename to osu.Game/Screens/Multi/Components/DrawableGameType.cs index 5790008f76..3406e179d4 100644 --- a/osu.Game/Screens/Multiplayer/DrawableGameType.cs +++ b/osu.Game/Screens/Multi/Components/DrawableGameType.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; -namespace osu.Game.Screens.Multiplayer +namespace osu.Game.Screens.Multi.Components { public class DrawableGameType : CircularContainer, IHasTooltip { diff --git a/osu.Game/Screens/Multi/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Components/DrawableRoom.cs new file mode 100644 index 0000000000..88a253d719 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/DrawableRoom.cs @@ -0,0 +1,276 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Users; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Multi.Components +{ + public class DrawableRoom : OsuClickableContainer, IStateful + { + private const float corner_radius = 5; + private const float selection_border_width = 4; + private const float transition_duration = 100; + private const float content_padding = 10; + private const float height = 100; + private const float side_strip_width = 5; + private const float cover_width = 145; + + private readonly Box selectionBox; + + private readonly Bindable nameBind = new Bindable(); + private readonly Bindable hostBind = new Bindable(); + private readonly Bindable statusBind = new Bindable(); + private readonly Bindable typeBind = new Bindable(); + private readonly Bindable beatmapBind = new Bindable(); + private readonly Bindable participantsBind = new Bindable(); + + public readonly Room Room; + + private SelectionState state; + public SelectionState State + { + get { return state; } + set + { + if (value == state) return; + state = value; + + if (state == SelectionState.Selected) + selectionBox.FadeIn(transition_duration); + else + selectionBox.FadeOut(transition_duration); + + StateChanged?.Invoke(State); + } + } + + public event Action StateChanged; + + public DrawableRoom(Room room) + { + Room = room; + + RelativeSizeAxes = Axes.X; + Height = height + selection_border_width * 2; + CornerRadius = corner_radius + selection_border_width / 2; + Masking = true; + + // create selectionBox here so State can be set before being loaded + selectionBox = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0f, + }; + + Action += () => State = SelectionState.Selected; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, LocalisationEngine localisation) + { + Box sideStrip; + Container coverContainer; + OsuSpriteText name, status, beatmapTitle, beatmapDash, beatmapArtist; + ParticipantInfo participantInfo; + ModeTypeInfo modeTypeInfo; + + Children = new Drawable[] + { + selectionBox, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(selection_border_width), + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(40), + Radius = 5, + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"212121"), + }, + sideStrip = new Box + { + RelativeSizeAxes = Axes.Y, + Width = side_strip_width, + }, + new Container + { + Width = cover_width, + RelativeSizeAxes = Axes.Y, + Masking = true, + Margin = new MarginPadding { Left = side_strip_width }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + coverContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Vertical = content_padding, + Left = side_strip_width + cover_width + content_padding, + Right = content_padding, + }, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5f), + Children = new Drawable[] + { + name = new OsuSpriteText + { + TextSize = 18, + }, + participantInfo = new ParticipantInfo(), + }, + }, + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + status = new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-Bold", + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Colour = colours.Gray9, + Direction = FillDirection.Horizontal, + Children = new[] + { + beatmapTitle = new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-BoldItalic", + }, + beatmapDash = new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-BoldItalic", + }, + beatmapArtist = new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-RegularItalic", + }, + }, + }, + }, + }, + modeTypeInfo = new ModeTypeInfo + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + }, + }, + }, + }, + }, + }; + + nameBind.ValueChanged += n => name.Text = n; + hostBind.ValueChanged += h => participantInfo.Host = h; + typeBind.ValueChanged += m => modeTypeInfo.Type = m; + participantsBind.ValueChanged += p => participantInfo.Participants = p; + + statusBind.ValueChanged += s => + { + status.Text = s.Message; + + foreach (Drawable d in new Drawable[] { selectionBox, sideStrip, status }) + d.FadeColour(s.GetAppropriateColour(colours), 100); + }; + + beatmapBind.ValueChanged += b => + { + modeTypeInfo.Beatmap = b; + + if (b != null) + { + coverContainer.FadeIn(transition_duration); + + LoadComponentAsync(new BeatmapSetCover(b.BeatmapSet) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), + }, coverContainer.Add); + + beatmapTitle.Current = localisation.GetUnicodePreference(b.Metadata.TitleUnicode, b.Metadata.Title); + beatmapDash.Text = @" - "; + beatmapArtist.Current = localisation.GetUnicodePreference(b.Metadata.ArtistUnicode, b.Metadata.Artist); + } + else + { + coverContainer.FadeOut(transition_duration); + + beatmapTitle.Current = null; + beatmapArtist.Current = null; + + beatmapTitle.Text = "Changing map"; + beatmapDash.Text = beatmapArtist.Text = string.Empty; + } + }; + + nameBind.BindTo(Room.Name); + hostBind.BindTo(Room.Host); + statusBind.BindTo(Room.Status); + typeBind.BindTo(Room.Type); + beatmapBind.BindTo(Room.Beatmap); + participantsBind.BindTo(Room.Participants); + } + } +} diff --git a/osu.Game/Screens/Multiplayer/ModeTypeInfo.cs b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs similarity index 98% rename from osu.Game/Screens/Multiplayer/ModeTypeInfo.cs rename to osu.Game/Screens/Multi/Components/ModeTypeInfo.cs index 08e96ba55d..e3aba685a7 100644 --- a/osu.Game/Screens/Multiplayer/ModeTypeInfo.cs +++ b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs @@ -1,14 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Online.Multiplayer; +using OpenTK; -namespace osu.Game.Screens.Multiplayer +namespace osu.Game.Screens.Multi.Components { public class ModeTypeInfo : Container { diff --git a/osu.Game/Screens/Multiplayer/ParticipantInfo.cs b/osu.Game/Screens/Multi/Components/ParticipantInfo.cs similarity index 99% rename from osu.Game/Screens/Multiplayer/ParticipantInfo.cs rename to osu.Game/Screens/Multi/Components/ParticipantInfo.cs index ff1887fa17..ab404488f1 100644 --- a/osu.Game/Screens/Multiplayer/ParticipantInfo.cs +++ b/osu.Game/Screens/Multi/Components/ParticipantInfo.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using OpenTK; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,8 +10,9 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Users; +using OpenTK; -namespace osu.Game.Screens.Multiplayer +namespace osu.Game.Screens.Multi.Components { public class ParticipantInfo : Container { diff --git a/osu.Game/Screens/Multiplayer/RoomInspector.cs b/osu.Game/Screens/Multi/Components/RoomInspector.cs similarity index 72% rename from osu.Game/Screens/Multiplayer/RoomInspector.cs rename to osu.Game/Screens/Multi/Components/RoomInspector.cs index bfc4a44ed5..3bd054b042 100644 --- a/osu.Game/Screens/Multiplayer/RoomInspector.cs +++ b/osu.Game/Screens/Multi/Components/RoomInspector.cs @@ -2,8 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; -using OpenTK; -using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; @@ -20,22 +18,16 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer; using osu.Game.Users; +using OpenTK; +using OpenTK.Graphics; -namespace osu.Game.Screens.Multiplayer +namespace osu.Game.Screens.Multi.Components { public class RoomInspector : Container { - private readonly MarginPadding contentPadding = new MarginPadding { Horizontal = 20, Vertical = 10 }; private const float transition_duration = 100; - private readonly Box statusStrip; - private readonly Container coverContainer; - private readonly FillFlowContainer topFlow, participantsFlow; - private readonly ModeTypeInfo modeTypeInfo; - private readonly OsuSpriteText participants, participantsSlash, maxParticipants, name, status, beatmapTitle, beatmapDash, beatmapArtist, beatmapAuthor; - private readonly ParticipantInfo participantInfo; - private readonly ScrollContainer participantsScroll; - + private readonly MarginPadding contentPadding = new MarginPadding { Horizontal = 20, Vertical = 10 }; private readonly Bindable nameBind = new Bindable(); private readonly Bindable hostBind = new Bindable(); private readonly Bindable statusBind = new Bindable(); @@ -45,10 +37,14 @@ namespace osu.Game.Screens.Multiplayer private readonly Bindable participantsBind = new Bindable(); private OsuColour colours; - private LocalisationEngine localisation; + private Box statusStrip; + private Container coverContainer; + private FillFlowContainer topFlow, participantsFlow, participantNumbersFlow, infoPanelFlow; + private OsuSpriteText name, status; + private ScrollContainer participantsScroll; + private ParticipantInfo participantInfo; private Room room; - public Room Room { get { return room; } @@ -57,20 +53,36 @@ namespace osu.Game.Screens.Multiplayer if (value == room) return; room = value; - nameBind.BindTo(Room.Name); - hostBind.BindTo(Room.Host); - statusBind.BindTo(Room.Status); - typeBind.BindTo(Room.Type); - beatmapBind.BindTo(Room.Beatmap); - maxParticipantsBind.BindTo(Room.MaxParticipants); - participantsBind.BindTo(Room.Participants); + nameBind.UnbindBindings(); + hostBind.UnbindBindings(); + statusBind.UnbindBindings(); + typeBind.UnbindBindings(); + beatmapBind.UnbindBindings(); + maxParticipantsBind.UnbindBindings(); + participantsBind.UnbindBindings(); + + if (room != null) + { + nameBind.BindTo(room.Name); + hostBind.BindTo(room.Host); + statusBind.BindTo(room.Status); + typeBind.BindTo(room.Type); + beatmapBind.BindTo(room.Beatmap); + maxParticipantsBind.BindTo(room.MaxParticipants); + participantsBind.BindTo(room.Participants); + } + + updateState(); } } - public RoomInspector() + [BackgroundDependencyLoader] + private void load(OsuColour colours, LocalisationEngine localisation) { - Width = 520; - RelativeSizeAxes = Axes.Y; + this.colours = colours; + + ModeTypeInfo modeTypeInfo; + OsuSpriteText participants, participantsSlash, maxParticipants, beatmapTitle, beatmapDash, beatmapArtist, beatmapAuthor; Children = new Drawable[] { @@ -120,7 +132,7 @@ namespace osu.Game.Screens.Multiplayer Padding = new MarginPadding(20), Children = new Drawable[] { - new FillFlowContainer + participantNumbersFlow = new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -178,6 +190,7 @@ namespace osu.Game.Screens.Multiplayer RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, + LayoutDuration = transition_duration, Padding = contentPadding, Spacing = new Vector2(0f, 5f), Children = new Drawable[] @@ -187,7 +200,7 @@ namespace osu.Game.Screens.Multiplayer TextSize = 14, Font = @"Exo2.0-Bold", }, - new FillFlowContainer + infoPanelFlow = new FillFlowContainer { AutoSizeAxes = Axes.X, Height = 30, @@ -229,6 +242,7 @@ namespace osu.Game.Screens.Multiplayer Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, TextSize = 14, + Colour = colours.Gray9, }, }, }, @@ -269,27 +283,68 @@ namespace osu.Game.Screens.Multiplayer }, }; - nameBind.ValueChanged += displayName; - hostBind.ValueChanged += displayUser; - typeBind.ValueChanged += displayGameType; - maxParticipantsBind.ValueChanged += displayMaxParticipants; - participantsBind.ValueChanged += displayParticipants; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, LocalisationEngine localisation) - { - this.localisation = localisation; - this.colours = colours; - - beatmapAuthor.Colour = colours.Gray9; - - //binded here instead of ctor because dependencies are needed + nameBind.ValueChanged += n => name.Text = n; + hostBind.ValueChanged += h => participantInfo.Host = h; + typeBind.ValueChanged += t => modeTypeInfo.Type = t; statusBind.ValueChanged += displayStatus; - beatmapBind.ValueChanged += displayBeatmap; - statusBind.TriggerChange(); - beatmapBind.TriggerChange(); + beatmapBind.ValueChanged += b => + { + modeTypeInfo.Beatmap = b; + + if (b != null) + { + coverContainer.FadeIn(transition_duration); + + LoadComponentAsync(new BeatmapSetCover(b.BeatmapSet) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), + }, coverContainer.Add); + + beatmapTitle.Current = localisation.GetUnicodePreference(b.Metadata.TitleUnicode, b.Metadata.Title); + beatmapDash.Text = @" - "; + beatmapArtist.Current = localisation.GetUnicodePreference(b.Metadata.ArtistUnicode, b.Metadata.Artist); + beatmapAuthor.Text = $"mapped by {b.Metadata.Author}"; + } + else + { + coverContainer.FadeOut(transition_duration); + + beatmapTitle.Current = null; + beatmapArtist.Current = null; + + beatmapTitle.Text = "Changing map"; + beatmapDash.Text = beatmapArtist.Text = beatmapAuthor.Text = string.Empty; + } + }; + + maxParticipantsBind.ValueChanged += m => + { + if (m == null) + { + participantsSlash.FadeOut(transition_duration); + maxParticipants.FadeOut(transition_duration); + } + else + { + participantsSlash.FadeIn(transition_duration); + maxParticipants.FadeIn(transition_duration); + maxParticipants.Text = m.ToString(); + } + }; + + participantsBind.ValueChanged += p => + { + participants.Text = p.Length.ToString(); + participantInfo.Participants = p; + participantsFlow.ChildrenEnumerable = p.Select(u => new UserTile(u)); + }; + + updateState(); } protected override void UpdateAfterChildren() @@ -299,84 +354,39 @@ namespace osu.Game.Screens.Multiplayer participantsScroll.Height = DrawHeight - topFlow.DrawHeight; } - private void displayName(string value) + private void displayStatus(RoomStatus s) { - name.Text = value; + status.Text = s.Message; + + Color4 c = s.GetAppropriateColour(colours); + statusStrip.FadeColour(c, transition_duration); + status.FadeColour(c, transition_duration); } - private void displayUser(User value) + private void updateState() { - participantInfo.Host = value; - } - - private void displayStatus(RoomStatus value) - { - status.Text = value.Message; - - foreach (Drawable d in new Drawable[] { statusStrip, status }) - d.FadeColour(value.GetAppropriateColour(colours), transition_duration); - } - - private void displayGameType(GameType value) - { - modeTypeInfo.Type = value; - } - - private void displayBeatmap(BeatmapInfo value) - { - modeTypeInfo.Beatmap = value; - - if (value != null) - { - coverContainer.FadeIn(transition_duration); - - LoadComponentAsync(new BeatmapSetCover(value.BeatmapSet) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), - }, - coverContainer.Add); - - beatmapTitle.Current = localisation.GetUnicodePreference(value.Metadata.TitleUnicode, value.Metadata.Title); - beatmapDash.Text = @" - "; - beatmapArtist.Current = localisation.GetUnicodePreference(value.Metadata.ArtistUnicode, value.Metadata.Artist); - beatmapAuthor.Text = $"mapped by {value.Metadata.Author}"; - } - else + if (Room == null) { coverContainer.FadeOut(transition_duration); + participantsFlow.FadeOut(transition_duration); + participantNumbersFlow.FadeOut(transition_duration); + infoPanelFlow.FadeOut(transition_duration); + name.FadeOut(transition_duration); + participantInfo.FadeOut(transition_duration); - beatmapTitle.Current = null; - beatmapArtist.Current = null; - - beatmapTitle.Text = "Changing map"; - beatmapDash.Text = beatmapArtist.Text = beatmapAuthor.Text = string.Empty; - } - } - - private void displayMaxParticipants(int? value) - { - if (value == null) - { - participantsSlash.FadeOut(transition_duration); - maxParticipants.FadeOut(transition_duration); + displayStatus(new RoomStatusNoneSelected()); } else { - participantsSlash.FadeIn(transition_duration); - maxParticipants.FadeIn(transition_duration); - maxParticipants.Text = value.ToString(); - } - } + participantsFlow.FadeIn(transition_duration); + participantNumbersFlow.FadeIn(transition_duration); + infoPanelFlow.FadeIn(transition_duration); + name.FadeIn(transition_duration); + participantInfo.FadeIn(transition_duration); - private void displayParticipants(User[] value) - { - participants.Text = value.Length.ToString(); - participantInfo.Participants = value; - participantsFlow.ChildrenEnumerable = value.Select(u => new UserTile(u)); + statusBind.TriggerChange(); + beatmapBind.TriggerChange(); + } } private class UserTile : Container, IHasTooltip @@ -407,5 +417,11 @@ namespace osu.Game.Screens.Multiplayer }; } } + + private class RoomStatusNoneSelected : RoomStatus + { + public override string Message => @"No Room Selected"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.Gray8; + } } } diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs new file mode 100644 index 0000000000..db8898495f --- /dev/null +++ b/osu.Game/Screens/Multi/Header.cs @@ -0,0 +1,112 @@ +// 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.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Screens; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.SearchableList; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Multi +{ + public class Header : Container + { + public const float HEIGHT = 121; + + private readonly OsuSpriteText screenTitle; + private readonly HeaderBreadcrumbControl breadcrumbs; + + public Header(Screen initialScreen) + { + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"2f2043"), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.BottomLeft, + Position = new Vector2(-35f, 5f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10f, 0f), + Children = new Drawable[] + { + new SpriteIcon + { + Size = new Vector2(25), + Icon = FontAwesome.fa_osu_multi, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + new OsuSpriteText + { + Text = "multiplayer ", + TextSize = 25, + }, + screenTitle = new OsuSpriteText + { + TextSize = 25, + Font = @"Exo2.0-Light", + }, + }, + }, + }, + }, + breadcrumbs = new HeaderBreadcrumbControl(initialScreen) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + }, + }, + }, + }; + + breadcrumbs.Current.ValueChanged += s => screenTitle.Text = s.ToString(); + breadcrumbs.Current.TriggerChange(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + screenTitle.Colour = colours.Yellow; + breadcrumbs.StripColour = colours.Green; + } + + private class HeaderBreadcrumbControl : ScreenBreadcrumbControl + { + public HeaderBreadcrumbControl(Screen initialScreen) : base(initialScreen) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + AccentColour = Color4.White; + } + } + } +} diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs new file mode 100644 index 0000000000..b3d393209c --- /dev/null +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -0,0 +1,100 @@ +// 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; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Screens; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.Multi.Screens; + +namespace osu.Game.Screens.Multi +{ + public class Multiplayer : OsuScreen + { + private readonly MultiplayerWaveContainer waves; + + protected override Container Content => waves; + + public Multiplayer() + { + InternalChild = waves = new MultiplayerWaveContainer + { + RelativeSizeAxes = Axes.Both, + }; + + Lobby lobby; + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"3e3a44"), + }, + new Triangles + { + RelativeSizeAxes = Axes.Both, + ColourLight = OsuColour.FromHex(@"3c3842"), + ColourDark = OsuColour.FromHex(@"393540"), + TriangleScale = 5, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = Header.HEIGHT }, + Child = lobby = new Lobby(), + }, + new Header(lobby), + }; + + lobby.Exited += s => Exit(); + } + + protected override void OnEntering(Screen last) + { + base.OnEntering(last); + waves.Show(); + } + + protected override bool OnExiting(Screen next) + { + waves.Hide(); + return base.OnExiting(next); + } + + protected override void OnResuming(Screen last) + { + base.OnResuming(last); + waves.Show(); + } + + protected override void OnSuspending(Screen next) + { + base.OnSuspending(next); + waves.Hide(); + } + + private class MultiplayerWaveContainer : WaveContainer + { + protected override bool StartHidden => true; + + public MultiplayerWaveContainer() + { + FirstWaveColour = OsuColour.FromHex(@"654d8c"); + SecondWaveColour = OsuColour.FromHex(@"554075"); + ThirdWaveColour = OsuColour.FromHex(@"44325e"); + FourthWaveColour = OsuColour.FromHex(@"392850"); + } + } + } +} diff --git a/osu.Game/Screens/Multiplayer/Lobby.cs b/osu.Game/Screens/Multi/Screens/Lobby.cs similarity index 90% rename from osu.Game/Screens/Multiplayer/Lobby.cs rename to osu.Game/Screens/Multi/Screens/Lobby.cs index 65fa5fbb16..dcda40e0d7 100644 --- a/osu.Game/Screens/Multiplayer/Lobby.cs +++ b/osu.Game/Screens/Multi/Screens/Lobby.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -namespace osu.Game.Screens.Multiplayer +namespace osu.Game.Screens.Multi.Screens { public class Lobby : ScreenWhiteBox { diff --git a/osu.Game/Screens/Multiplayer/Match.cs b/osu.Game/Screens/Multi/Screens/Match.cs similarity index 96% rename from osu.Game/Screens/Multiplayer/Match.cs rename to osu.Game/Screens/Multi/Screens/Match.cs index 5402e70ea5..4ba7fe9f6a 100644 --- a/osu.Game/Screens/Multiplayer/Match.cs +++ b/osu.Game/Screens/Multi/Screens/Match.cs @@ -3,14 +3,14 @@ using System; using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; -using OpenTK.Graphics; using osu.Game.Screens.Select; -using osu.Framework.Graphics; +using OpenTK.Graphics; -namespace osu.Game.Screens.Multiplayer +namespace osu.Game.Screens.Multi.Screens { public class Match : ScreenWhiteBox { diff --git a/osu.Game/Screens/Multiplayer/MatchCreate.cs b/osu.Game/Screens/Multi/Screens/MatchCreate.cs similarity index 91% rename from osu.Game/Screens/Multiplayer/MatchCreate.cs rename to osu.Game/Screens/Multi/Screens/MatchCreate.cs index ca6b814cb9..6b4e26d5e5 100644 --- a/osu.Game/Screens/Multiplayer/MatchCreate.cs +++ b/osu.Game/Screens/Multi/Screens/MatchCreate.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -namespace osu.Game.Screens.Multiplayer +namespace osu.Game.Screens.Multi.Screens { public class MatchCreate : ScreenWhiteBox { diff --git a/osu.Game/Screens/Multiplayer/DrawableRoom.cs b/osu.Game/Screens/Multiplayer/DrawableRoom.cs deleted file mode 100644 index d53100526f..0000000000 --- a/osu.Game/Screens/Multiplayer/DrawableRoom.cs +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Multiplayer; -using osu.Game.Users; - -namespace osu.Game.Screens.Multiplayer -{ - public class DrawableRoom : OsuClickableContainer - { - private const float transition_duration = 100; - private const float content_padding = 10; - private const float height = 100; - private const float side_strip_width = 5; - private const float cover_width = 145; - - private readonly Box sideStrip; - private readonly Container coverContainer; - private readonly OsuSpriteText name, status, beatmapTitle, beatmapDash, beatmapArtist; - private readonly FillFlowContainer beatmapInfoFlow; - private readonly ParticipantInfo participantInfo; - private readonly ModeTypeInfo modeTypeInfo; - - private readonly Bindable nameBind = new Bindable(); - private readonly Bindable hostBind = new Bindable(); - private readonly Bindable statusBind = new Bindable(); - private readonly Bindable typeBind = new Bindable(); - private readonly Bindable beatmapBind = new Bindable(); - private readonly Bindable participantsBind = new Bindable(); - - private OsuColour colours; - private LocalisationEngine localisation; - - public readonly Room Room; - - public DrawableRoom(Room room) - { - Room = room; - - RelativeSizeAxes = Axes.X; - Height = height; - CornerRadius = 5; - Masking = true; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(40), - Radius = 5, - }; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"212121"), - }, - sideStrip = new Box - { - RelativeSizeAxes = Axes.Y, - Width = side_strip_width, - }, - new Container - { - Width = cover_width, - RelativeSizeAxes = Axes.Y, - Masking = true, - Margin = new MarginPadding { Left = side_strip_width }, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - coverContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Vertical = content_padding, - Left = side_strip_width + cover_width + content_padding, - Right = content_padding, - }, - Children = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5f), - Children = new Drawable[] - { - name = new OsuSpriteText - { - TextSize = 18, - }, - participantInfo = new ParticipantInfo(), - }, - }, - new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - status = new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-Bold", - }, - beatmapInfoFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new[] - { - beatmapTitle = new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-BoldItalic", - }, - beatmapDash = new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-BoldItalic", - }, - beatmapArtist = new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-RegularItalic", - }, - }, - }, - }, - }, - modeTypeInfo = new ModeTypeInfo - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }, - }, - }, - }; - - nameBind.ValueChanged += displayName; - hostBind.ValueChanged += displayUser; - typeBind.ValueChanged += displayGameType; - participantsBind.ValueChanged += displayParticipants; - - nameBind.BindTo(Room.Name); - hostBind.BindTo(Room.Host); - statusBind.BindTo(Room.Status); - typeBind.BindTo(Room.Type); - beatmapBind.BindTo(Room.Beatmap); - participantsBind.BindTo(Room.Participants); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, LocalisationEngine localisation) - { - this.localisation = localisation; - this.colours = colours; - - beatmapInfoFlow.Colour = colours.Gray9; - - //binded here instead of ctor because dependencies are needed - statusBind.ValueChanged += displayStatus; - beatmapBind.ValueChanged += displayBeatmap; - - statusBind.TriggerChange(); - beatmapBind.TriggerChange(); - } - - private void displayName(string value) - { - name.Text = value; - } - - private void displayUser(User value) - { - participantInfo.Host = value; - } - - private void displayStatus(RoomStatus value) - { - if (value == null) return; - status.Text = value.Message; - - foreach (Drawable d in new Drawable[] { sideStrip, status }) - d.FadeColour(value.GetAppropriateColour(colours), 100); - } - - private void displayGameType(GameType value) - { - modeTypeInfo.Type = value; - } - - private void displayBeatmap(BeatmapInfo value) - { - modeTypeInfo.Beatmap = value; - - if (value != null) - { - coverContainer.FadeIn(transition_duration); - - LoadComponentAsync(new BeatmapSetCover(value.BeatmapSet) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), - }, - coverContainer.Add); - - beatmapTitle.Current = localisation.GetUnicodePreference(value.Metadata.TitleUnicode, value.Metadata.Title); - beatmapDash.Text = @" - "; - beatmapArtist.Current = localisation.GetUnicodePreference(value.Metadata.ArtistUnicode, value.Metadata.Artist); - } - else - { - coverContainer.FadeOut(transition_duration); - - beatmapTitle.Current = null; - beatmapArtist.Current = null; - - beatmapTitle.Text = "Changing map"; - beatmapDash.Text = beatmapArtist.Text = string.Empty; - } - } - - private void displayParticipants(User[] value) - { - participantInfo.Participants = value; - } - } -} diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 7a910574e0..24945ea347 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -3,25 +3,29 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; -using OpenTK; -using osu.Framework.Audio.Sample; -using osu.Framework.Audio; -using osu.Framework.Graphics; +using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Screens.Menu; -using osu.Framework.Input; +using OpenTK; using OpenTK.Input; namespace osu.Game.Screens { - public abstract class OsuScreen : Screen + public abstract class OsuScreen : Screen, IKeyBindingHandler { public BackgroundScreen Background { get; private set; } + protected virtual bool AllowBackButton => true; + /// /// Override to create a BackgroundMode for the current screen. /// Note that the instance created may not be the used instance if it matches the BackgroundMode equality clause. @@ -90,6 +94,19 @@ namespace osu.Game.Screens sampleExit = audio.Sample.Get(@"UI/screen-back"); } + public bool OnPressed(GlobalAction action) + { + if (action == GlobalAction.Back && AllowBackButton) + { + Exit(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => action == GlobalAction.Back && AllowBackButton; + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { if (args.Repeat || !IsCurrentScreen) return false; diff --git a/osu.Game/Screens/Play/HotkeyRetryOverlay.cs b/osu.Game/Screens/Play/HotkeyRetryOverlay.cs index a018a2697a..926a96eb6c 100644 --- a/osu.Game/Screens/Play/HotkeyRetryOverlay.cs +++ b/osu.Game/Screens/Play/HotkeyRetryOverlay.cs @@ -1,50 +1,19 @@ // 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 System; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Game.Input.Bindings; -using OpenTK.Graphics; +using osu.Game.Overlays; namespace osu.Game.Screens.Play { - public class HotkeyRetryOverlay : Container, IKeyBindingHandler + public class HotkeyRetryOverlay : HoldToConfirmOverlay, IKeyBindingHandler { - public Action Action; - - private Box overlay; - - private const int activate_delay = 400; - private const int fadeout_delay = 200; - - private bool fired; - - [BackgroundDependencyLoader] - private void load() - { - RelativeSizeAxes = Axes.Both; - AlwaysPresent = true; - - Children = new Drawable[] - { - overlay = new Box - { - Alpha = 0, - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - } - }; - } - public bool OnPressed(GlobalAction action) { if (action != GlobalAction.QuickRetry) return false; - overlay.FadeIn(activate_delay, Easing.Out); + BeginConfirm(); return true; } @@ -52,18 +21,8 @@ namespace osu.Game.Screens.Play { if (action != GlobalAction.QuickRetry) return false; - overlay.FadeOut(fadeout_delay, Easing.Out); + AbortConfirm(); return true; } - - protected override void Update() - { - base.Update(); - if (!fired && overlay.Alpha == 1) - { - fired = true; - Action?.Invoke(); - } - } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 83958b2912..46919e25e1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -45,6 +45,8 @@ namespace osu.Game.Screens.Play public bool AllowLeadIn { get; set; } = true; public bool AllowResults { get; set; } = true; + protected override bool AllowBackButton => false; + private Bindable mouseWheelDisabled; private Bindable userAudioOffset; @@ -93,7 +95,7 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); - Beatmap beatmap; + IBeatmap beatmap; try { @@ -107,7 +109,7 @@ namespace osu.Game.Screens.Play try { - RulesetContainer = rulesetInstance.CreateRulesetContainerWith(working, ruleset.ID == beatmap.BeatmapInfo.Ruleset.ID); + RulesetContainer = rulesetInstance.CreateRulesetContainerWith(working); } catch (BeatmapInvalidForRulesetException) { @@ -115,7 +117,7 @@ namespace osu.Game.Screens.Play // let's try again forcing the beatmap's ruleset. ruleset = beatmap.BeatmapInfo.Ruleset; rulesetInstance = ruleset.CreateInstance(); - RulesetContainer = rulesetInstance.CreateRulesetContainerWith(Beatmap, true); + RulesetContainer = rulesetInstance.CreateRulesetContainerWith(Beatmap); } if (!RulesetContainer.Objects.Any()) @@ -162,7 +164,7 @@ namespace osu.Game.Screens.Play hudOverlay.KeyCounter.IsCounting = pauseContainer.IsPaused; }, OnResume = () => hudOverlay.KeyCounter.IsCounting = true, - Children = new Drawable[] + Children = new[] { storyboardContainer = new Container { @@ -174,12 +176,12 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Child = RulesetContainer }, - new SkipOverlay(firstObjectTime) + new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, scoreProcessor) { - Clock = Clock, // skip button doesn't want to use the audio clock directly + Anchor = Anchor.Centre, + Origin = Anchor.Centre, ProcessCustomClock = false, - AdjustableClock = adjustableClock, - FramedClock = offsetClock, + Breaks = beatmap.Breaks }, hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, working, offsetClock, adjustableClock) { @@ -188,13 +190,14 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre }, - new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, scoreProcessor) + RulesetContainer.Cursor?.CreateProxy() ?? new Container(), + new SkipOverlay(firstObjectTime) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Clock = Clock, // skip button doesn't want to use the audio clock directly ProcessCustomClock = false, - Breaks = beatmap.Breaks - } + AdjustableClock = adjustableClock, + FramedClock = offsetClock, + }, } }, failOverlay = new FailOverlay diff --git a/osu.Game/Screens/Play/SongProgressInfo.cs b/osu.Game/Screens/Play/SongProgressInfo.cs index 5cc4b30950..b79c212ade 100644 --- a/osu.Game/Screens/Play/SongProgressInfo.cs +++ b/osu.Game/Screens/Play/SongProgressInfo.cs @@ -85,11 +85,13 @@ namespace osu.Game.Screens.Play if (currentSecond != previousSecond && songCurrentTime < songLength) { - timeCurrent.Text = TimeSpan.FromSeconds(currentSecond).ToString(songCurrentTime < 0 ? @"\-m\:ss" : @"m\:ss"); - timeLeft.Text = TimeSpan.FromMilliseconds(endTime - AudioClock.CurrentTime).ToString(@"\-m\:ss"); + timeCurrent.Text = formatTime(TimeSpan.FromSeconds(currentSecond)); + timeLeft.Text = formatTime(TimeSpan.FromMilliseconds(endTime - AudioClock.CurrentTime)); previousSecond = currentSecond; } } + + private string formatTime(TimeSpan timeSpan) => $"{(timeSpan < TimeSpan.Zero ? "-" : "")}{timeSpan.Duration().TotalMinutes:N0}:{timeSpan.Duration().Seconds:D2}"; } } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index f005261ffa..97f6371cb2 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -21,6 +23,8 @@ using osu.Game.Rulesets.Objects.Types; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; +using osu.Game.Rulesets; +using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Select { @@ -28,6 +32,8 @@ namespace osu.Game.Screens.Select { private static readonly Vector2 wedged_container_shear = new Vector2(0.15f, 0); + private readonly IBindable ruleset = new Bindable(); + protected BufferedWedgeInfo Info; public BeatmapInfoWedge() @@ -46,6 +52,14 @@ namespace osu.Game.Screens.Select }; } + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] OsuGame osuGame) + { + if (osuGame != null) + ruleset.BindTo(osuGame.Ruleset); + ruleset.ValueChanged += updateRuleset; + } + protected override bool BlockPassThroughMouse => false; protected override void PopIn() @@ -62,19 +76,39 @@ namespace osu.Game.Screens.Select this.FadeOut(500, Easing.In); } + private WorkingBeatmap beatmap; + public void UpdateBeatmap(WorkingBeatmap beatmap) { - LoadComponentAsync(new BufferedWedgeInfo(beatmap) - { - Shear = -Shear, - Depth = Info?.Depth + 1 ?? 0, - }, newInfo => + this.beatmap = beatmap; + loadBeatmap(); + } + + private void updateRuleset(RulesetInfo ruleset) => loadBeatmap(); + + private void loadBeatmap() + { + void updateState() { State = beatmap == null ? Visibility.Hidden : Visibility.Visible; Info?.FadeOut(250); Info?.Expire(); + } + if (beatmap == null) + { + updateState(); + return; + } + + LoadComponentAsync(new BufferedWedgeInfo(beatmap, ruleset.Value) + { + Shear = -Shear, + Depth = Info?.Depth + 1 ?? 0, + }, newInfo => + { + updateState(); Add(Info = newInfo); }); } @@ -90,9 +124,13 @@ namespace osu.Game.Screens.Select private UnicodeBindableString titleBinding; private UnicodeBindableString artistBinding; - public BufferedWedgeInfo(WorkingBeatmap working) + private readonly RulesetInfo ruleset; + + public BufferedWedgeInfo(WorkingBeatmap working, RulesetInfo userRuleset) { this.working = working; + + ruleset = userRuleset ?? working.BeatmapInfo.Ruleset; } [BackgroundDependencyLoader] @@ -211,11 +249,10 @@ namespace osu.Game.Screens.Select private InfoLabel[] getInfoLabels() { var beatmap = working.Beatmap; - var info = working.BeatmapInfo; List labels = new List(); - if (beatmap?.HitObjects?.Count > 0) + if (beatmap?.HitObjects?.Any() == true) { HitObject lastObject = beatmap.HitObjects.LastOrDefault(); double endTime = (lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0; @@ -224,7 +261,7 @@ namespace osu.Game.Screens.Select { Name = "Length", Icon = FontAwesome.fa_clock_o, - Content = beatmap.HitObjects.Count == 0 ? "-" : TimeSpan.FromMilliseconds(endTime - beatmap.HitObjects.First().StartTime).ToString(@"m\:ss"), + Content = TimeSpan.FromMilliseconds(endTime - beatmap.HitObjects.First().StartTime).ToString(@"m\:ss"), })); labels.Add(new InfoLabel(new BeatmapStatistic @@ -234,14 +271,26 @@ namespace osu.Game.Screens.Select Content = getBPMRange(beatmap), })); - //get statistics from the current ruleset. - labels.AddRange(info.Ruleset.CreateInstance().GetBeatmapStatistics(working).Select(s => new InfoLabel(s))); + IBeatmap playableBeatmap; + + try + { + // Try to get the beatmap with the user's ruleset + playableBeatmap = working.GetPlayableBeatmap(ruleset); + } + catch (BeatmapInvalidForRulesetException) + { + // Can't be converted to the user's ruleset, so use the beatmap's own ruleset + playableBeatmap = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset); + } + + labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s))); } return labels.ToArray(); } - private string getBPMRange(Beatmap beatmap) + private string getBPMRange(IBeatmap beatmap) { double bpmMax = beatmap.ControlPointInfo.BPMMaximum; double bpmMin = beatmap.ControlPointInfo.BPMMinimum; diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index ee458a13a4..f9f3db3827 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -190,7 +190,5 @@ namespace osu.Game.Screens.Select protected override bool OnMouseMove(InputState state) => true; protected override bool OnClick(InputState state) => true; - - protected override bool OnDragStart(InputState state) => true; } } diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 363249ab63..8f07e0a763 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -141,7 +141,5 @@ namespace osu.Game.Screens.Select protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; protected override bool OnClick(InputState state) => true; - - protected override bool OnDragStart(InputState state) => true; } } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 4e252eac75..3ffac591f3 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -7,7 +7,12 @@ namespace osu.Game.Screens.Select { protected override bool OnSelectionFinalised() { - Exit(); + Schedule(() => + { + // needs to be scheduled else we enter an infinite feedback loop. + if (IsCurrentScreen) Exit(); + }); + return true; } } diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 2850de8ba5..7470f6ebed 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; +using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; namespace osu.Game.Tests.Beatmaps @@ -79,9 +80,12 @@ namespace osu.Game.Tests.Beatmaps { var beatmap = getBeatmap(name); + var rulesetInstance = CreateRuleset(); + beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo(); + var result = new ConvertResult(); - var converter = CreateConverter(beatmap); + var converter = rulesetInstance.CreateBeatmapConverter(beatmap); converter.ObjectConverted += (orig, converted) => { converted.ForEach(h => h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty)); @@ -92,7 +96,7 @@ namespace osu.Game.Tests.Beatmaps result.Mappings.Add(mapping); }; - converter.Convert(beatmap); + converter.Convert(); return result; } @@ -107,7 +111,7 @@ namespace osu.Game.Tests.Beatmaps } } - private Beatmap getBeatmap(string name) + private IBeatmap getBeatmap(string name) { using (var resStream = openResource($"{resource_namespace}.{name}.osu")) using (var stream = new StreamReader(resStream)) @@ -125,7 +129,7 @@ namespace osu.Game.Tests.Beatmaps } protected abstract IEnumerable CreateConvertValue(HitObject hitObject); - protected abstract IBeatmapConverter CreateConverter(Beatmap beatmap); + protected abstract Ruleset CreateRuleset(); private class ConvertMapping { diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 09a3a7af8c..6bad08baaa 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -12,8 +12,14 @@ namespace osu.Game.Tests.Beatmaps public class TestBeatmap : Beatmap { public TestBeatmap(RulesetInfo ruleset) - : base(createTestBeatmap()) { + var baseBeatmap = createTestBeatmap(); + + BeatmapInfo = baseBeatmap.BeatmapInfo; + ControlPointInfo = baseBeatmap.ControlPointInfo; + Breaks = baseBeatmap.Breaks; + HitObjects = baseBeatmap.HitObjects; + BeatmapInfo.Ruleset = ruleset; } diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index e24fbab3ac..37693c99e8 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -17,14 +17,14 @@ namespace osu.Game.Tests.Beatmaps { } - public TestWorkingBeatmap(Beatmap beatmap) + public TestWorkingBeatmap(IBeatmap beatmap) : base(beatmap.BeatmapInfo) { this.beatmap = beatmap; } - private readonly Beatmap beatmap; - protected override Beatmap GetBeatmap() => beatmap; + private readonly IBeatmap beatmap; + protected override IBeatmap GetBeatmap() => beatmap; protected override Texture GetBackground() => null; protected override Track GetTrack() diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs index 29132258c2..51460ecb6d 100644 --- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs +++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs @@ -259,9 +259,9 @@ namespace osu.Game.Tests.Visual private readonly OsuSpriteText text; private readonly Score score; - private readonly Beatmap beatmap; + private readonly IBeatmap beatmap; - public PerformanceDisplay(Score score, Beatmap beatmap) + public PerformanceDisplay(Score score, IBeatmap beatmap) { this.score = score; this.beatmap = beatmap; diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs index 5ed43b2814..bda438d906 100644 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ b/osu.Game/Tests/Visual/TestCasePlayer.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual } } - protected virtual Beatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo); + protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo); private Player loadPlayerFor(RulesetInfo ri) => loadPlayerFor(ri.CreateInstance());