Merge pull request #17270 from frenzibyte/multiplayer-disable-adaptive-speed

Disable mod "Adaptive Speed" in multiplayer
This commit is contained in:
Dean Herbert 2022-05-09 17:31:47 +09:00 committed by GitHub
commit ab1d46b71c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 244 additions and 15 deletions

View File

@ -137,33 +137,137 @@ namespace osu.Game.Tests.Mods
// incompatible pair. // incompatible pair.
new object[] new object[]
{ {
new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() }, new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) } new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) }
}, },
// incompatible pair with derived class. // incompatible pair with derived class.
new object[] new object[]
{ {
new Mod[] { new OsuModNightcore(), new OsuModHalfTime() }, new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() },
new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) } new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) }
}, },
// system mod. // system mod.
new object[] new object[]
{ {
new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() }, new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) } new[] { typeof(OsuModTouchDevice) }
}, },
// multi mod. // multi mod.
new object[] new object[]
{ {
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() }, new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) },
new[] { typeof(MultiMod) } new[] { typeof(MultiMod) }
}, },
// invalid multiplayer mod is valid for local.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() },
null
},
// invalid free mod is valid for local.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
null
},
// valid pair. // valid pair.
new object[] new object[]
{ {
new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() }, new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null null
} },
};
private static readonly object[] invalid_multiplayer_mod_test_scenarios =
{
// incompatible pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) }
},
// incompatible pair with derived class.
new object[]
{
new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() },
new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) }
},
// system mod.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) }
},
// multi mod.
new object[]
{
new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) },
new[] { typeof(MultiMod) }
},
// invalid multiplayer mod.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() },
new[] { typeof(InvalidMultiplayerMod) }
},
// invalid free mod is valid for multiplayer global.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
null
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null
},
};
private static readonly object[] invalid_free_mod_test_scenarios =
{
// system mod.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) }
},
// multi mod.
new object[]
{
new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) },
new[] { typeof(MultiMod) }
},
// invalid multiplayer mod.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() },
new[] { typeof(InvalidMultiplayerMod) }
},
// invalid free mod.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
new[] { typeof(InvalidMultiplayerFreeMod) }
},
// incompatible pair is valid for free mods.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
null,
},
// incompatible pair with derived class is valid for free mods.
new object[]
{
new Mod[] { new OsuModDeflate(), new OsuModSpinIn() },
null,
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null
},
}; };
[TestCaseSource(nameof(invalid_mod_test_scenarios))] [TestCaseSource(nameof(invalid_mod_test_scenarios))]
@ -179,6 +283,32 @@ namespace osu.Game.Tests.Mods
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
} }
[TestCaseSource(nameof(invalid_multiplayer_mod_test_scenarios))]
public void TestInvalidMultiplayerModScenarios(Mod[] inputMods, Type[] expectedInvalid)
{
bool isValid = ModUtils.CheckValidRequiredModsForMultiplayer(inputMods, out var invalid);
Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
if (isValid)
Assert.IsNull(invalid);
else
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
[TestCaseSource(nameof(invalid_free_mod_test_scenarios))]
public void TestInvalidFreeModScenarios(Mod[] inputMods, Type[] expectedInvalid)
{
bool isValid = ModUtils.CheckValidFreeModsForMultiplayer(inputMods, out var invalid);
Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
if (isValid)
Assert.IsNull(invalid);
else
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
{ {
} }
@ -187,6 +317,27 @@ namespace osu.Game.Tests.Mods
{ {
} }
public class InvalidMultiplayerMod : Mod
{
public override string Name => string.Empty;
public override string Description => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
public override bool HasImplementation => true;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
}
private class InvalidMultiplayerFreeMod : Mod
{
public override string Name => string.Empty;
public override string Description => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
public override bool HasImplementation => true;
public override bool ValidForMultiplayerAsFreeMod => false;
}
public interface IModCompatibilitySpecification public interface IModCompatibilitySpecification
{ {
} }

View File

@ -39,6 +39,18 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
bool UserPlayable { get; } bool UserPlayable { get; }
/// <summary>
/// Whether this mod is valid for multiplayer matches.
/// Should be <c>false</c> for mods that make gameplay duration dependent on user input (e.g. <see cref="ModAdaptiveSpeed"/>).
/// </summary>
bool ValidForMultiplayer { get; }
/// <summary>
/// Whether this mod is valid as a free mod in multiplayer matches.
/// Should be <c>false</c> for mods that affect the gameplay duration (e.g. <see cref="ModRateAdjust"/> and <see cref="ModTimeRamp"/>).
/// </summary>
bool ValidForMultiplayerAsFreeMod { get; }
/// <summary> /// <summary>
/// Create a fresh <see cref="Mod"/> instance based on this mod. /// Create a fresh <see cref="Mod"/> instance based on this mod.
/// </summary> /// </summary>

View File

@ -94,6 +94,12 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore] [JsonIgnore]
public virtual bool UserPlayable => true; public virtual bool UserPlayable => true;
[JsonIgnore]
public virtual bool ValidForMultiplayer => true;
[JsonIgnore]
public virtual bool ValidForMultiplayerAsFreeMod => true;
[Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009 [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009
public virtual bool Ranked => false; public virtual bool Ranked => false;

View File

@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) };
[SettingSource("Initial rate", "The starting speed of the track")] [SettingSource("Initial rate", "The starting speed of the track")]

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mods
public bool RestartOnFail => false; public bool RestartOnFail => false;
public override bool UserPlayable => false; public override bool UserPlayable => false;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };

View File

@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModRateAdjust : Mod, IApplicableToRate public abstract class ModRateAdjust : Mod, IApplicableToRate
{ {
public override bool ValidForMultiplayerAsFreeMod => false;
public abstract BindableNumber<double> SpeedChange { get; } public abstract BindableNumber<double> SpeedChange { get; }
public virtual void ApplyToTrack(ITrack track) public virtual void ApplyToTrack(ITrack track)

View File

@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mods
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public abstract BindableBool AdjustPitch { get; } public abstract BindableBool AdjustPitch { get; }
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };
public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";

View File

@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 0; public override double ScoreMultiplier => 0;
public override bool UserPlayable => false; public override bool UserPlayable => false;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override ModType Type => ModType.System; public override ModType Type => ModType.System;

View File

@ -95,6 +95,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer;
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod;
} }
} }

View File

@ -106,22 +106,69 @@ namespace osu.Game.Utils
} }
/// <summary> /// <summary>
/// Check the provided combination of mods are valid for a local gameplay session. /// Checks that all <see cref="Mod"/>s in a combination are valid for a local gameplay session.
/// </summary> /// </summary>
/// <param name="mods">The mods to check.</param> /// <param name="mods">The mods to check.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Can be null if all mods were valid.</param> /// <param name="invalidMods">Invalid mods, if any were found. Will be null if all mods were valid.</param>
/// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns> /// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns>
public static bool CheckValidForGameplay(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods) public static bool CheckValidForGameplay(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods)
{ {
mods = mods.ToArray(); mods = mods.ToArray();
// exclude multi mods from compatibility checks. // checking compatibility of multi mods would try to flatten them and return incompatible mods.
// the loop below automatically marks all multi mods as not valid for gameplay anyway. // in gameplay context, we never want MultiMod selected in the first place, therefore check against it first.
CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods); if (!checkValid(mods, m => !(m is MultiMod), out invalidMods))
return false;
if (!CheckCompatibleSet(mods, out invalidMods))
return false;
return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods);
}
/// <summary>
/// Checks that all <see cref="Mod"/>s in a combination are valid as "required mods" in a multiplayer match session.
/// </summary>
/// <param name="mods">The mods to check.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Will be null if all mods were valid.</param>
/// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns>
public static bool CheckValidRequiredModsForMultiplayer(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods)
{
mods = mods.ToArray();
// checking compatibility of multi mods would try to flatten them and return incompatible mods.
// in gameplay context, we never want MultiMod selected in the first place, therefore check against it first.
if (!checkValid(mods, m => !(m is MultiMod), out invalidMods))
return false;
if (!CheckCompatibleSet(mods, out invalidMods))
return false;
return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayer, out invalidMods);
}
/// <summary>
/// Checks that all <see cref="Mod"/>s in a combination are valid as "free mods" in a multiplayer match session.
/// </summary>
/// <remarks>
/// Note that this does not check compatibility between mods,
/// given that the passed mods are expected to be the ones to be allowed for the multiplayer match,
/// not to be confused with the list of mods the user currently has selected for the multiplayer match.
/// </remarks>
/// <param name="mods">The mods to check.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Will be null if all mods were valid.</param>
/// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns>
public static bool CheckValidFreeModsForMultiplayer(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods)
=> checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayerAsFreeMod && !(m is MultiMod), out invalidMods);
private static bool checkValid(IEnumerable<Mod> mods, Predicate<Mod> valid, [NotNullWhen(false)] out List<Mod>? invalidMods)
{
mods = mods.ToArray();
invalidMods = null;
foreach (var mod in mods) foreach (var mod in mods)
{ {
if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod) if (!valid(mod))
{ {
invalidMods ??= new List<Mod>(); invalidMods ??= new List<Mod>();
invalidMods.Add(mod); invalidMods.Add(mod);