1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 15:53:51 +08:00

Merge pull request #17191 from peppy/fix-mod-conversion-exceptions

Change `ToMod` to return an `UnknownMod` rather than throw if a mod isn't available
This commit is contained in:
Dan Balasescu 2022-03-10 16:20:50 +09:00 committed by GitHub
commit 13a4058efd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 15 deletions

View File

@ -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<APIMod>(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()
{

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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();
/// <summary>
/// This test also covers the scenario of exiting Player after an unsuccessful beatmap load.
/// </summary>
[Test]
public void TestUnknownModDoesntEnterGameplay()
{
CreateModTest(new ModTestData
{
Beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Beatmap,
Mod = new UnknownMod("WNG"),
PassCondition = () => Player.IsLoaded && !Player.LoadedBeatmapSuccessfully
});
}
}
}

View File

@ -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<string, object> obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value));
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// The acronym of the mod which could not be resolved.
/// </summary>
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);
}
}

View File

@ -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.

View File

@ -119,7 +119,8 @@ namespace osu.Game.Screens.Play
{
bool exiting = base.OnExiting(next);
submitScore(Score.DeepClone());
if (LoadedBeatmapSuccessfully)
submitScore(Score.DeepClone());
return exiting;
}