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 1483283283..2517839355 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -59,7 +59,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty double starRating = calculateDifficulty() * star_scaling_factor; - categoryDifficulty?.Add("Strain", starRating); + if (categoryDifficulty != null) + categoryDifficulty["Strain"] = starRating; return starRating; } 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/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b1702de537..02ecb3afda 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -18,6 +18,7 @@ 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 { @@ -25,6 +26,7 @@ namespace osu.Game.Rulesets.Mania { 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) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 3661c0be04..8791e8ed86 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -204,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) @@ -216,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/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 99% rename from osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs rename to osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 84a83d331b..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 { 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 93f1b7faf7..11586e340b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests 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); 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/TestCaseDrawableRoom.cs b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs index bb5bf93a69..8fd8880fd6 100644 --- a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs +++ b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs @@ -6,6 +6,7 @@ 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.Multi.Components; @@ -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/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/TestCaseRoomInspector.cs b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs index cb1425ca69..06b9c4a6f9 100644 --- a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs +++ b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs @@ -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/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/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 9c389bbb8f..66a6206c16 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -107,8 +107,14 @@ namespace osu.Game.Beatmaps IBeatmap converted = converter.Convert(); // Apply difficulty mods - foreach (var mod in Mods.Value.OfType()) - mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); + 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(); 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 1658bb4e79..fced2e5d95 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -37,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[] @@ -80,5 +83,8 @@ namespace osu.Game.Input.Bindings TakeScreenshot, [Description("Toggle gameplay mouse buttons")] ToggleGameplayMouseButtons, + + [Description("Go back")] + Back } } 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/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/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index 9fd7a0156d..07d9c80061 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Difficulty private readonly Dictionary attributes = new Dictionary(); protected IDictionary Attributes => attributes; + protected readonly Ruleset Ruleset; protected readonly IBeatmap Beatmap; protected readonly Score Score; @@ -23,9 +24,9 @@ namespace osu.Game.Rulesets.Difficulty protected PerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) { - Score = score; - + Ruleset = ruleset; Beatmap = beatmap; + Score = score; var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); diffCalc.Calculate(attributes); 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/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/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/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index e564ab786d..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.Multi.Screens; +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; @@ -54,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/Multi/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Components/DrawableRoom.cs index 040fbaf593..88a253d719 100644 --- a/osu.Game/Screens/Multi/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Components/DrawableRoom.cs @@ -1,6 +1,8 @@ // 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; @@ -13,6 +15,7 @@ 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; @@ -20,20 +23,17 @@ using OpenTK.Graphics; namespace osu.Game.Screens.Multi.Components { - public class DrawableRoom : OsuClickableContainer + 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 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 Box selectionBox; private readonly Bindable nameBind = new Bindable(); private readonly Bindable hostBind = new Bindable(); @@ -42,136 +42,228 @@ namespace osu.Game.Screens.Multi.Components private readonly Bindable beatmapBind = new Bindable(); private readonly Bindable participantsBind = new Bindable(); - private OsuColour colours; - private LocalisationEngine localisation; - 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; - CornerRadius = 5; + Height = height + selection_border_width * 2; + CornerRadius = corner_radius + selection_border_width / 2; Masking = true; - EdgeEffect = new EdgeEffectParameters + + // create selectionBox here so State can be set before being loaded + selectionBox = new Box { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(40), - Radius = 5, + 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[] { - 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, - }, - }, - }, + selectionBox, new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + Padding = new MarginPadding(selection_border_width), + Child = new Container { - Vertical = content_padding, - Left = side_strip_width + cover_width + content_padding, - Right = content_padding, - }, - Children = new Drawable[] - { - new FillFlowContainer + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + EdgeEffect = new EdgeEffectParameters { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5f), - Children = new Drawable[] + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(40), + Radius = 5, + }, + Children = new Drawable[] + { + new Box { - name = new OsuSpriteText - { - TextSize = 18, - }, - participantInfo = new ParticipantInfo(), + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"212121"), }, - }, - new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + sideStrip = new Box { - status = new OsuSpriteText + 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[] { - TextSize = 14, - Font = @"Exo2.0-Bold", - }, - beatmapInfoFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new[] + new Box { - beatmapTitle = new OsuSpriteText + 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[] { - TextSize = 14, - Font = @"Exo2.0-BoldItalic", + name = new OsuSpriteText + { + TextSize = 18, + }, + participantInfo = new ParticipantInfo(), }, - beatmapDash = new OsuSpriteText + }, + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - TextSize = 14, - Font = @"Exo2.0-BoldItalic", - }, - beatmapArtist = new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-RegularItalic", + 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, + }, }, }, }, - modeTypeInfo = new ModeTypeInfo - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }, }, }, }; - nameBind.ValueChanged += displayName; - hostBind.ValueChanged += displayUser; - typeBind.ValueChanged += displayGameType; - participantsBind.ValueChanged += displayParticipants; + 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); @@ -180,83 +272,5 @@ namespace osu.Game.Screens.Multi.Components 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/Multi/Components/RoomInspector.cs b/osu.Game/Screens/Multi/Components/RoomInspector.cs index 92910e8301..3bd054b042 100644 --- a/osu.Game/Screens/Multi/Components/RoomInspector.cs +++ b/osu.Game/Screens/Multi/Components/RoomInspector.cs @@ -25,17 +25,9 @@ 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.Multi.Components 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.Multi.Components 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.Multi.Components 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.Multi.Components 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.Multi.Components TextSize = 14, Font = @"Exo2.0-Bold", }, - new FillFlowContainer + infoPanelFlow = new FillFlowContainer { AutoSizeAxes = Axes.X, Height = 30, @@ -229,6 +242,7 @@ namespace osu.Game.Screens.Multi.Components Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, TextSize = 14, + Colour = colours.Gray9, }, }, }, @@ -269,27 +283,68 @@ namespace osu.Game.Screens.Multi.Components }, }; - 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.Multi.Components 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.Multi.Components }; } } + + 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/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/Player.cs b/osu.Game/Screens/Play/Player.cs index 4a46279d30..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; 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; } }