Dean Herbert 0267aed846 Change ToMod to return an UnknownMod rather than throw if a mod isn't available
As seen by this kind of crash, having the `.ToMod` method throw can be
very problematic and also hidden (as it is used inside of models in
places where exceptions are not expected to occur).

Given there are tens of usages of this method, returning a placeholder
mod seems like a better idea than outright throwing.

```
 An unhandled has occurred.
 System.InvalidOperationException:
There is no mod in the ruleset (osu) matching the acronym AS.
 at osu.Game.Online.API.APIMod.ToMod(Ruleset ruleset) in /Users/dean/Projects/osu/osu.Game/Online/API/APIMod.cs:line 54
 at osu.Game.Scoring.ScoreInfo.<get_Mods>b__117_0(APIMod m) in /Users/dean/Projects/osu/osu.Game/Scoring/ScoreInfo.cs:line 193
 at System.Linq.Enumerable.SelectArrayIterator`2.ToArray()
 at osu.Game.Scoring.ScoreInfo.get_Mods() in /Users/dean/Projects/osu/osu.Game/Scoring/ScoreInfo.cs:line 193
 at osu.Game.Screens.Select.Leaderboards.BeatmapLeaderboard.<>c.<subscribeToLocalScores>b__40_2(ScoreInfo s) in /Users/dean/Projects/osu/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs:line 199
 at System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()
 at osu.Game.Database.RealmObjectExtensions.Detach[T](IEnumerable`1 items) in /Users/dean/Projects/osu/osu.Game/Database/RealmObjectExtensions.cs:line 180
 at osu.Game.Screens.Select.Leaderboards.BeatmapLeaderboard.<>c__DisplayClass40_0.<subscribeToLocalScores>g__localScoresChanged|1(IRealmCollection`1 sender, ChangeSet changes, Exception exception) in /Users/dean/Projects/osu/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs:line
209
 at Realms.RealmCollectionBase`1.Realms.INotifiable<Realms.NotifiableObjectHandleBase.CollectionChangeSet>.NotifyCallbacks(Nullable`1 changes, Nullable`1 exception)
 at Realms.NotifiableObjectHandleBase.NotifyObjectChanged(IntPtr managedHandle, IntPtr changes, IntPtr exception)
```
2022-03-09 17:57:55 +09:00

107 lines
3.4 KiB
C#

// 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 System;
using System.Collections.Generic;
using System.Linq;
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;
using osu.Game.Utils;
namespace osu.Game.Online.API
{
[MessagePackObject]
public class APIMod : IEquatable<APIMod>
{
[JsonProperty("acronym")]
[Key(0)]
public string Acronym { get; set; }
[JsonProperty("settings")]
[Key(1)]
[MessagePackFormatter(typeof(ModSettingsDictionaryFormatter))]
public Dictionary<string, object> Settings { get; set; } = new Dictionary<string, object>();
[JsonConstructor]
[SerializationConstructor]
public APIMod()
{
}
public APIMod(Mod mod)
{
Acronym = mod.Acronym;
foreach (var (_, property) in mod.GetSettingsSourceProperties())
{
var bindable = (IBindable)property.GetValue(mod);
if (!bindable.IsDefault)
Settings.Add(property.Name.Underscore(), ModUtils.GetSettingUnderlyingValue(bindable));
}
}
public Mod ToMod(Ruleset ruleset)
{
Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
if (resultMod == null)
{
Logger.Log($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}.");
return new UnknownMod(Acronym);
}
if (Settings.Count > 0)
{
foreach (var (_, property) in resultMod.GetSettingsSourceProperties())
{
if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
continue;
resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue);
}
}
return resultMod;
}
public bool Equals(APIMod other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Acronym == other.Acronym && Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
}
public override string ToString()
{
if (Settings.Count > 0)
return $"{Acronym} ({string.Join(',', Settings.Select(kvp => $"{kvp.Key}:{kvp.Value}"))})";
return $"{Acronym}";
}
private class ModSettingsEqualityComparer : IEqualityComparer<KeyValuePair<string, object>>
{
public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer();
public bool Equals(KeyValuePair<string, object> x, KeyValuePair<string, object> y)
{
object xValue = ModUtils.GetSettingUnderlyingValue(x.Value);
object yValue = ModUtils.GetSettingUnderlyingValue(y.Value);
return x.Key == y.Key && EqualityComparer<object>.Default.Equals(xValue, yValue);
}
public int GetHashCode(KeyValuePair<string, object> obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value));
}
}
}