diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index d5ea3e492c..e98ea98bb2 100644 --- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -24,6 +24,20 @@ namespace osu.Game.Tests.Online [TestFixture] public class TestAPIModJsonSerialization { + [Test] + public void TestUnknownMod() + { + var apiMod = new APIMod { Acronym = "WNG" }; + + var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); + + var converted = deserialized?.ToMod(new TestRuleset()); + + Assert.That(converted, Is.TypeOf(typeof(UnknownMod))); + Assert.That(converted?.Type, Is.EqualTo(ModType.System)); + Assert.That(converted?.Acronym, Is.EqualTo("WNG??")); + } + [Test] public void TestAcronymIsPreserved() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs new file mode 100644 index 0000000000..c0f1112905 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneUnknownMod.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneUnknownMod : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + + /// + /// This test also covers the scenario of exiting Player after an unsuccessful beatmap load. + /// + [Test] + public void TestUnknownModDoesntEnterGameplay() + { + CreateModTest(new ModTestData + { + Beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Beatmap, + Mod = new UnknownMod("WNG"), + PassCondition = () => Player.IsLoaded && !Player.LoadedBeatmapSuccessfully + }); + } + } +} diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 67041cae07..ef8637b8ed 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -8,6 +8,7 @@ using Humanizer; using MessagePack; using Newtonsoft.Json; using osu.Framework.Bindables; +using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -51,7 +52,10 @@ namespace osu.Game.Online.API Mod resultMod = ruleset.CreateModFromAcronym(Acronym); if (resultMod == null) - throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}."); + { + Logger.Log($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}."); + return new UnknownMod(Acronym); + } if (Settings.Count > 0) { @@ -98,4 +102,5 @@ namespace osu.Game.Online.API public int GetHashCode(KeyValuePair obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value)); } } + } diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs new file mode 100644 index 0000000000..b426386d7a --- /dev/null +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Mods +{ + public class UnknownMod : Mod + { + /// + /// The acronym of the mod which could not be resolved. + /// + public readonly string OriginalAcronym; + + public override string Name => $"Unknown mod ({OriginalAcronym})"; + public override string Acronym => $"{OriginalAcronym}??"; + public override string Description => "This mod could not be resolved by the game."; + public override double ScoreMultiplier => 0; + + public override bool UserPlayable => false; + + public override ModType Type => ModType.System; + + public UnknownMod(string acronym) + { + OriginalAcronym = acronym; + } + + public override Mod DeepClone() => new UnknownMod(OriginalAcronym); + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b6f576ff2b..cb8f4b6020 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -185,6 +185,12 @@ namespace osu.Game.Screens.Play { var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray(); + if (gameplayMods.Any(m => m is UnknownMod)) + { + Logger.Log("Gameplay was started with an unknown mod applied.", level: LogLevel.Important); + return; + } + if (Beatmap.Value is DummyWorkingBeatmap) return; @@ -988,24 +994,27 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { - if (!GameplayState.HasPassed && !GameplayState.HasFailed) - GameplayState.HasQuit = true; - screenSuspension?.RemoveAndDisposeImmediately(); failAnimationLayer?.RemoveFilters(); - // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. - if (prepareScoreForDisplayTask == null) + if (LoadedBeatmapSuccessfully) { - Score.ScoreInfo.Passed = false; - // potentially should be ScoreRank.F instead? this is the best alternative for now. - Score.ScoreInfo.Rank = ScoreRank.D; - } + if (!GameplayState.HasPassed && !GameplayState.HasFailed) + GameplayState.HasQuit = true; - // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. - // To resolve test failures, forcefully end playing synchronously when this screen exits. - // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. - spectatorClient.EndPlaying(GameplayState); + // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. + if (prepareScoreForDisplayTask == null) + { + Score.ScoreInfo.Passed = false; + // potentially should be ScoreRank.F instead? this is the best alternative for now. + Score.ScoreInfo.Rank = ScoreRank.D; + } + + // EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous. + // To resolve test failures, forcefully end playing synchronously when this screen exits. + // Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method. + spectatorClient.EndPlaying(GameplayState); + } // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track. // as we are no longer the current screen, we cannot guarantee the track is still usable. diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 2cf56be659..b1f2bccddf 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -119,7 +119,8 @@ namespace osu.Game.Screens.Play { bool exiting = base.OnExiting(next); - submitScore(Score.DeepClone()); + if (LoadedBeatmapSuccessfully) + submitScore(Score.DeepClone()); return exiting; }