1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 06:42:54 +08:00

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.
new object[]
{
new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() },
new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) }
new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) }
},
// incompatible pair with derived class.
new object[]
{
new Mod[] { new OsuModNightcore(), new OsuModHalfTime() },
new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) }
new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() },
new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) }
},
// system mod.
new object[]
{
new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() },
new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) }
},
// multi mod.
new object[]
{
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() },
new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) },
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.
new object[]
{
new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() },
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
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))]
@ -179,6 +283,32 @@ namespace osu.Game.Tests.Mods
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
{
}
@ -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
{
}

View File

@ -39,6 +39,18 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
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>
/// Create a fresh <see cref="Mod"/> instance based on this mod.
/// </summary>

View File

@ -94,6 +94,12 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
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
public virtual bool Ranked => false;

View File

@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) };
[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 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) };

View File

@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Mods
{
public abstract class ModRateAdjust : Mod, IApplicableToRate
{
public override bool ValidForMultiplayerAsFreeMod => false;
public abstract BindableNumber<double> SpeedChange { get; }
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")]
public abstract BindableBool AdjustPitch { get; }
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };
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 bool UserPlayable => false;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
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 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>
/// 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>
/// <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>
public static bool CheckValidForGameplay(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods)
{
mods = mods.ToArray();
// exclude multi mods from compatibility checks.
// the loop below automatically marks all multi mods as not valid for gameplay anyway.
CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods);
// 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, 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)
{
if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod)
if (!valid(mod))
{
invalidMods ??= new List<Mod>();
invalidMods.Add(mod);