diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 808faa511b..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 { - internal class CatchBeatmapConversionTest : BeatmapConversionTest + internal class CatchBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; @@ -47,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests } } - protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap); + protected override Ruleset CreateRuleset() => new CatchRuleset(); } internal struct ConvertValue : IEquatable @@ -64,8 +62,4 @@ namespace osu.Game.Rulesets.Catch.Tests => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) && Precision.AlmostEquals(Position, other.Position, conversion_lenience); } - - internal class TestCatchRuleset : CatchRuleset - { - } } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index bd67a7d96a..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,7 +12,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests { - internal class ManiaBeatmapConversionTest : BeatmapConversionTest + internal class ManiaBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; @@ -35,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests }; } - protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap); + protected override Ruleset CreateRuleset() => new ManiaRuleset(); } internal struct ConvertValue : IEquatable @@ -54,8 +52,4 @@ namespace osu.Game.Rulesets.Mania.Tests && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) && Column == other.Column; } - - internal class TestManiaRuleset : ManiaRuleset - { - } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 4f7c52860f..19fef9eb54 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -84,10 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps yield break; foreach (ManiaHitObject obj in objects) - { - obj.HitWindows = original.HitWindows; yield return obj; - } } private readonly List prevNoteTimes = new List(max_notes_for_density); diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 8458e63fca..2517839355 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania.Beatmaps; @@ -49,11 +50,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty int columnCount = (Beatmap as ManiaBeatmap)?.TotalColumns ?? 7; - foreach (var hitObject in Beatmap.HitObjects) - difficultyHitObjects.Add(new ManiaHitObjectDifficulty((ManiaHitObject)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; 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/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 12e3d2de51..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,25 +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. - /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps - /// - 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/Objects/Legacy/Mania/ConvertHitWindows.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs similarity index 88% rename from osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitWindows.cs rename to osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs index 131492ea12..063b626af1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitWindows.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Objects.Legacy.Mania +namespace osu.Game.Rulesets.Mania.Objects { - public class ConvertHitWindows : HitWindows + public class ManiaHitWindows : HitWindows { private static readonly IReadOnlyDictionary base_ranges = new Dictionary { @@ -21,6 +22,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania 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]); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index 3d54043027..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 { - internal class OsuBeatmapConversionTest : BeatmapConversionTest + internal class OsuBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; @@ -42,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; } - protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap); + protected override Ruleset CreateRuleset() => new OsuRuleset(); } internal struct ConvertValue : IEquatable @@ -67,8 +65,4 @@ namespace osu.Game.Rulesets.Osu.Tests && Precision.AlmostEquals(EndX, other.EndX, conversion_lenience) && Precision.AlmostEquals(EndY, other.EndY, conversion_lenience); } - - internal class TestOsuRuleset : OsuRuleset - { - } } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 4369a31b2c..80eb808f6e 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -40,8 +40,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps RepeatSamples = curveData.RepeatSamples, RepeatCount = curveData.RepeatCount, Position = positionData?.Position ?? Vector2.Zero, - NewCombo = comboData?.NewCombo ?? false, - HitWindows = original.HitWindows + NewCombo = comboData?.NewCombo ?? false }; } else if (endTimeData != null) @@ -51,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, - HitWindows = original.HitWindows + Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2 }; } else @@ -62,8 +60,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps StartTime = original.StartTime, Samples = original.Samples, Position = positionData?.Position ?? Vector2.Zero, - NewCombo = comboData?.NewCombo ?? false, - HitWindows = original.HitWindows + NewCombo = comboData?.NewCombo ?? false }; } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs similarity index 90% rename from osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs rename to osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 938060a664..eeb776fa6e 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -11,7 +11,7 @@ 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 { @@ -32,9 +32,9 @@ namespace osu.Game.Rulesets.Osu.Scoring 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, IBeatmap beatmap, Score score) @@ -52,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 @@ -71,10 +71,10 @@ namespace osu.Game.Rulesets.Osu.Scoring ar = Math.Max(0, ar / 2); double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / TimeRate; - double hitWindow300 = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / 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 - hitWindow300) / 6; + 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 @@ -190,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; @@ -213,7 +213,7 @@ namespace osu.Game.Rulesets.Osu.Scoring return accuracyValue; } - private double totalHits => count300 + count100 + count50 + countMiss; - private double totalSuccessfulHits => count300 + count100 + count50; + private double totalHits => countGreat + countGood + countMeh + countMiss; + private double totalSuccessfulHits => countGreat + countGood + countMeh; } } 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/Objects/Legacy/Osu/ConvertHitWindows.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs similarity index 90% rename from osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitWindows.cs rename to osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs index fd86173372..8405498554 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitWindows.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Objects.Legacy.Osu +namespace osu.Game.Rulesets.Osu.Objects { - public class ConvertHitWindows : HitWindows + public class OsuHitWindows : HitWindows { private static readonly IReadOnlyDictionary base_ranges = new Dictionary { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index a2423ffbe5..c455bb2af6 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -12,7 +12,6 @@ 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.Osu.Replays; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index ca4fc3ec57..11586e340b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -5,22 +5,20 @@ 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 { - internal class TaikoBeatmapConversionTest : BeatmapConversionTest + internal class TaikoBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; [NonParallelizable] - [TestCase("basic", false), Ignore("See: https://github.com/ppy/osu/issues/2152")] - [TestCase("slider-generating-drumroll", false)] + [TestCase("basic")] + [TestCase("slider-generating-drumroll")] public new void Test(string name) { base.Test(name); @@ -40,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests }; } - protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap); + protected override Ruleset CreateRuleset() => new TaikoRuleset(); } internal struct ConvertValue : IEquatable @@ -67,8 +65,4 @@ namespace osu.Game.Rulesets.Taiko.Tests && IsSwell == other.IsSwell && IsStrong == other.IsStrong; } - - internal class TestTaikoRuleset : TaikoRuleset - { - } } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index b450e4d26c..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; @@ -51,8 +50,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps 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); @@ -98,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; @@ -132,8 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = j, Samples = currentSamples, - IsStrong = strong, - HitWindows = obj.HitWindows + IsStrong = strong }; } else @@ -142,8 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = j, Samples = currentSamples, - IsStrong = strong, - HitWindows = obj.HitWindows + IsStrong = strong }; } @@ -158,8 +156,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps Samples = obj.Samples, IsStrong = strong, Duration = taikoDuration, - TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4, - HitWindows = obj.HitWindows + TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4 }; } } @@ -173,8 +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), - HitWindows = obj.HitWindows + RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier) }; } else @@ -187,8 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - IsStrong = strong, - HitWindows = obj.HitWindows + IsStrong = strong }; } else @@ -197,8 +192,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - IsStrong = strong, - HitWindows = obj.HitWindows + IsStrong = strong }; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index acff0d286d..57e1e65064 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -5,6 +5,7 @@ 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.Difficulty @@ -35,6 +36,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } + 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 @@ -51,10 +57,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty 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; } 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/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/Objects/Legacy/Taiko/ConvertHitWindows.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs similarity index 90% rename from osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitWindows.cs rename to osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs index 6fbf7e122f..289f084a45 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitWindows.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; -namespace osu.Game.Rulesets.Objects.Legacy.Taiko +namespace osu.Game.Rulesets.Taiko.Objects { - public class ConvertHitWindows : HitWindows + public class TaikoHitWindows : HitWindows { private static readonly IReadOnlyDictionary base_ranges = new Dictionary { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 35dc17c0e2..abaa8db597 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -14,6 +14,7 @@ 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; @@ -144,7 +145,9 @@ namespace osu.Game.Rulesets.Taiko public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o }; - public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap 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.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/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 84897853d8..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; @@ -55,17 +54,11 @@ namespace osu.Game.Beatmaps IBeatmap IBeatmap.Clone() => Clone(); - public Beatmap Clone() - { - var newInstance = (Beatmap)MemberwiseClone(); - newInstance.BeatmapInfo = BeatmapInfo.DeepClone(); - - return newInstance; - } + public Beatmap Clone() => (Beatmap)MemberwiseClone(); } public class Beatmap : Beatmap { - public Beatmap Clone() => (Beatmap)base.Clone(); + public new Beatmap Clone() => (Beatmap)base.Clone(); } } diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index b7a454460f..a1bb70135a 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -53,8 +53,6 @@ namespace osu.Game.Beatmaps { 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(); 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/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/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/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/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index cd612a5387..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; @@ -56,10 +57,10 @@ namespace osu.Game.Rulesets.Objects /// 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. @@ -70,13 +71,19 @@ namespace osu.Game.Rulesets.Objects { ApplyDefaultsToSelf(controlPointInfo, difficulty); - nestedHitObjects.Clear(); + if (nestedHitObjects.IsValueCreated) + nestedHitObjects.Value.Clear(); + CreateNestedHitObjects(); - nestedHitObjects.ForEach(h => + + if (nestedHitObjects.IsValueCreated) { - h.HitWindows = HitWindows; - h.ApplyDefaults(controlPointInfo, difficulty); - }); + nestedHitObjects.Value.ForEach(h => + { + h.HitWindows = HitWindows; + h.ApplyDefaults(controlPointInfo, difficulty); + }); + } } protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) @@ -96,7 +103,7 @@ namespace osu.Game.Rulesets.Objects { } - protected void AddNested(HitObject hitObject) => nestedHitObjects.Add(hitObject); + protected void AddNested(HitObject hitObject) => nestedHitObjects.Value.Add(hitObject); /// /// Creates the for this . diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs index 7610593d6a..3717209860 100644 --- a/osu.Game/Rulesets/Objects/HitWindows.cs +++ b/osu.Game/Rulesets/Objects/HitWindows.cs @@ -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 939d3b9c93..ea4e7f6907 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + 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 22abc64b60..86a10fd363 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs @@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public double Duration => EndTime - StartTime; - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + 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 6bca5b717c..a8d7b23df1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + 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 1dc826af9b..5a443c2ac2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania public float X { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + 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 23955b2d23..f015272b2c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + 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 35b8c1c7dd..ec5a002bbb 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + 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 73b8369aca..0141785f31 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -21,6 +21,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float Y => Position.Y; - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + 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 11db086778..5e9786c84a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs @@ -12,6 +12,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko { public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + 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 95c69222b5..8a9a0db0a7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs @@ -12,6 +12,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko { public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + 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 7baea212ea..4c8807a1d3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko public double Duration => EndTime - StartTime; - protected override HitWindows CreateHitWindows() => new ConvertHitWindows(); + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs index efd901240a..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; @@ -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/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index e564ab786d..600fad8da5 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.Multi.Screens; +using osu.Game.Screens.Multi; using osu.Game.Screens.Select; using osu.Game.Screens.Tournament; @@ -54,7 +54,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/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/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 2de38d49a9..7470f6ebed 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -16,8 +16,7 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Tests.Beatmaps { [TestFixture] - public abstract class BeatmapConversionTest - where TRuleset : Ruleset, new() + public abstract class BeatmapConversionTest where TConvertValue : IEquatable { private const string resource_namespace = "Testing.Beatmaps"; @@ -81,12 +80,12 @@ namespace osu.Game.Tests.Beatmaps { var beatmap = getBeatmap(name); - var rulesetInstance = new TRuleset(); + 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)); @@ -130,7 +129,7 @@ namespace osu.Game.Tests.Beatmaps } protected abstract IEnumerable CreateConvertValue(HitObject hitObject); - protected abstract IBeatmapConverter CreateConverter(IBeatmap beatmap); + protected abstract Ruleset CreateRuleset(); private class ConvertMapping {