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:
commit
ab1d46b71c
@ -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
|
||||
{
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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")]
|
||||
|
@ -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) };
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user