mirror of
https://github.com/ppy/osu.git
synced 2026-05-27 05:09:58 +08:00
Merge pull request #32674 from smoogipoo/freestyle-mods
Allow mods/freemods in combination with freestyle
This commit is contained in:
@@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override bool Ranked => false;
|
||||
|
||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||
|
||||
[SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")]
|
||||
public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => "FI";
|
||||
public override LocalisableString Description => @"Keys appear out of nowhere!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||
{
|
||||
|
||||
+130
-149
@@ -2,14 +2,20 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@@ -182,98 +188,6 @@ namespace osu.Game.Tests.Mods
|
||||
},
|
||||
};
|
||||
|
||||
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() },
|
||||
Array.Empty<Type>()
|
||||
},
|
||||
// valid pair.
|
||||
new object[]
|
||||
{
|
||||
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
|
||||
Array.Empty<Type>()
|
||||
},
|
||||
};
|
||||
|
||||
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() },
|
||||
Array.Empty<Type>(),
|
||||
},
|
||||
// incompatible pair with derived class is valid for free mods.
|
||||
new object[]
|
||||
{
|
||||
new Mod[] { new OsuModDeflate(), new OsuModSpinIn() },
|
||||
Array.Empty<Type>(),
|
||||
},
|
||||
// valid pair.
|
||||
new object[]
|
||||
{
|
||||
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
|
||||
Array.Empty<Type>()
|
||||
},
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(invalid_mod_test_scenarios))]
|
||||
public void TestInvalidModScenarios(Mod[] inputMods, Type[] expectedInvalid)
|
||||
{
|
||||
@@ -287,32 +201,6 @@ 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.Length == 0));
|
||||
|
||||
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.Length == 0));
|
||||
|
||||
if (isValid)
|
||||
Assert.IsNull(invalid);
|
||||
else
|
||||
Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModBelongsToRuleset()
|
||||
{
|
||||
@@ -343,38 +231,127 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRoomModValidity()
|
||||
private static readonly object[] multiplayer_mod_test_scenarios =
|
||||
{
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.Playlists));
|
||||
// valid - as allowed mod.
|
||||
new MultiplayerTestScenario(false, false, [new OsuModBarrelRoll()], []),
|
||||
new MultiplayerTestScenario(false, true, [new OsuModBarrelRoll()], []),
|
||||
// valid - as allowed mod (incompatible pair).
|
||||
new MultiplayerTestScenario(false, false, [new OsuModHardRock(), new OsuModEasy()], []),
|
||||
new MultiplayerTestScenario(false, true, [new OsuModHardRock(), new OsuModEasy()], []),
|
||||
// valid - as allowed mod (incompatible pair with derived classes).
|
||||
new MultiplayerTestScenario(false, false, [new OsuModDeflate(), new OsuModApproachDifferent()], []),
|
||||
new MultiplayerTestScenario(false, true, [new OsuModDeflate(), new OsuModApproachDifferent()], []),
|
||||
// valid - as allowed mod (not implemented in all rulesets).
|
||||
new MultiplayerTestScenario(false, false, [new OsuModBarrelRoll()], []),
|
||||
new MultiplayerTestScenario(false, true, [new OsuModBarrelRoll()], []),
|
||||
// valid - as required mod.
|
||||
new MultiplayerTestScenario(true, false, [new OsuModStrictTracking()], []),
|
||||
// valid - as required mod when not freestyle.
|
||||
new MultiplayerTestScenario(true, false, [new InvalidFreestyleRequiredMod()], []),
|
||||
// valid - as required mod when freestyle (implemented in all rulesets).
|
||||
new MultiplayerTestScenario(true, true, [new OsuModEasy()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModNoFail()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModHalfTime()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModDaycore()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModHardRock()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModSuddenDeath()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModPerfect()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModDoubleTime()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModNightcore()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModHidden()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModFlashlight()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModAccuracyChallenge()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModDifficultyAdjust()], []),
|
||||
new MultiplayerTestScenario(true, true, [new ModWindUp()], []),
|
||||
new MultiplayerTestScenario(true, true, [new ModWindDown()], []),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModMuted()], []),
|
||||
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModHardRock(), MatchType.HeadToHead));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead));
|
||||
// For now, adaptive speed isn't allowed in multiplayer because it's a per-user rate adjustment.
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead));
|
||||
// invalid - always (system mod)
|
||||
new MultiplayerTestScenario(false, false, [new OsuModTouchDevice()], [typeof(OsuModTouchDevice)]),
|
||||
new MultiplayerTestScenario(true, false, [new OsuModTouchDevice()], [typeof(OsuModTouchDevice)]),
|
||||
// invalid - always (multi mod).
|
||||
new MultiplayerTestScenario(false, false, [new MultiMod()], [typeof(MultiMod)]),
|
||||
new MultiplayerTestScenario(true, false, [new MultiMod()], [typeof(MultiMod)]),
|
||||
// invalid - always (disallowed by mod)
|
||||
new MultiplayerTestScenario(false, false, [new InvalidMultiplayerMod()], [typeof(InvalidMultiplayerMod)]),
|
||||
new MultiplayerTestScenario(true, false, [new InvalidMultiplayerMod()], [typeof(InvalidMultiplayerMod)]),
|
||||
new MultiplayerTestScenario(false, false, [new OsuModAutoplay()], [typeof(OsuModAutoplay)]),
|
||||
new MultiplayerTestScenario(true, false, [new OsuModAutoplay()], [typeof(OsuModAutoplay)]),
|
||||
// invalid - always (changes play length - for now not allowed in multiplayer).
|
||||
new MultiplayerTestScenario(false, false, [new ModAdaptiveSpeed()], [typeof(ModAdaptiveSpeed)]),
|
||||
new MultiplayerTestScenario(true, false, [new ModAdaptiveSpeed()], [typeof(ModAdaptiveSpeed)]),
|
||||
// invalid - as allowed mod (disallowed by mod).
|
||||
new MultiplayerTestScenario(false, false, [new InvalidMultiplayerFreeMod()], [typeof(InvalidMultiplayerFreeMod)]),
|
||||
new MultiplayerTestScenario(false, true, [new InvalidMultiplayerFreeMod()], [typeof(InvalidMultiplayerFreeMod)]),
|
||||
// invalid - as allowed mod (changes play length - for now not allowed in multiplayer).
|
||||
new MultiplayerTestScenario(false, false, [new OsuModHalfTime()], [typeof(OsuModHalfTime)]),
|
||||
new MultiplayerTestScenario(false, false, [new OsuModDaycore()], [typeof(OsuModDaycore)]),
|
||||
new MultiplayerTestScenario(false, false, [new OsuModDoubleTime()], [typeof(OsuModDoubleTime)]),
|
||||
new MultiplayerTestScenario(false, false, [new OsuModNightcore()], [typeof(OsuModNightcore)]),
|
||||
// invalid - as required mod (incompatible pair)
|
||||
new MultiplayerTestScenario(true, false, [new OsuModHidden(), new OsuModApproachDifferent()], [typeof(OsuModHidden), typeof(OsuModApproachDifferent)]),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModHidden(), new OsuModApproachDifferent()], [typeof(OsuModHidden), typeof(OsuModApproachDifferent)]),
|
||||
new MultiplayerTestScenario(true, false, [new OsuModDeflate(), new OsuModApproachDifferent()], [typeof(OsuModDeflate), typeof(OsuModApproachDifferent)]),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModDeflate(), new OsuModApproachDifferent()], [typeof(OsuModDeflate), typeof(OsuModApproachDifferent)]),
|
||||
// invalid - as required mod when freestyle (disallowed by mod).
|
||||
new MultiplayerTestScenario(true, true, [new InvalidFreestyleRequiredMod()], [typeof(InvalidFreestyleRequiredMod)]),
|
||||
// invalid - as required mod when freestyle (not implemented in all rulesets).
|
||||
new MultiplayerTestScenario(true, true, [new OsuModStrictTracking()], [typeof(OsuModStrictTracking)]),
|
||||
new MultiplayerTestScenario(true, true, [new OsuModBarrelRoll()], [typeof(OsuModBarrelRoll)]),
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(multiplayer_mod_test_scenarios))]
|
||||
public void TestMultiplayerModScenarios(MultiplayerTestScenario scenario)
|
||||
{
|
||||
List<Mod>? invalidMods;
|
||||
bool isValid = scenario.IsRequired
|
||||
? ModUtils.CheckValidRequiredModsForMultiplayer(scenario.Mods, scenario.IsFreestyle, out invalidMods)
|
||||
: ModUtils.CheckValidAllowedModsForMultiplayer(scenario.Mods, scenario.IsFreestyle, out invalidMods);
|
||||
|
||||
Assert.That(isValid, Is.EqualTo(scenario.InvalidTypes.Length == 0));
|
||||
|
||||
if (isValid)
|
||||
Assert.IsNull(invalidMods);
|
||||
else
|
||||
Assert.That(invalidMods?.Select(t => t.GetType()), Is.EquivalentTo(scenario.InvalidTypes));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRoomFreeModValidity()
|
||||
public void TestPlaylistsModScenarios()
|
||||
{
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.Playlists));
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.Playlists));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.Playlists));
|
||||
// The rest are tested by TestMultiplayerModScenarios.
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatch(new OsuModHardRock(), false, MatchType.Playlists, false));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatch(new OsuModHardRock(), true, MatchType.Playlists, false));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatch(new OsuModDoubleTime(), false, MatchType.Playlists, false));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatch(new OsuModDoubleTime(), true, MatchType.Playlists, false));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatch(new ModAdaptiveSpeed(), false, MatchType.Playlists, false));
|
||||
Assert.IsTrue(ModUtils.IsValidModForMatch(new ModAdaptiveSpeed(), true, MatchType.Playlists, false));
|
||||
}
|
||||
|
||||
Assert.IsTrue(ModUtils.IsValidFreeModForMatchType(new OsuModHardRock(), MatchType.HeadToHead));
|
||||
// For now, all rate adjustment mods aren't allowed as free mods in multiplayer.
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModDoubleTime(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new ModAdaptiveSpeed(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModAutoplay(), MatchType.HeadToHead));
|
||||
Assert.IsFalse(ModUtils.IsValidFreeModForMatchType(new OsuModTouchDevice(), MatchType.HeadToHead));
|
||||
[Test]
|
||||
public void TestFreestyleRulesetCompatibility()
|
||||
{
|
||||
HashSet<string> commonAcronyms = new HashSet<string>();
|
||||
|
||||
commonAcronyms.UnionWith(new OsuRuleset().CreateAllMods().Select(m => m.Acronym));
|
||||
commonAcronyms.IntersectWith(new TaikoRuleset().CreateAllMods().Select(m => m.Acronym));
|
||||
commonAcronyms.IntersectWith(new CatchRuleset().CreateAllMods().Select(m => m.Acronym));
|
||||
commonAcronyms.IntersectWith(new ManiaRuleset().CreateAllMods().Select(m => m.Acronym));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var ruleset in new Ruleset[] { new OsuRuleset(), new TaikoRuleset(), new CatchRuleset(), new ManiaRuleset() })
|
||||
{
|
||||
foreach (var mod in ruleset.CreateAllMods())
|
||||
{
|
||||
if (mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && !commonAcronyms.Contains(mod.Acronym))
|
||||
Assert.Fail($"{mod.GetType().ReadableName()} declares {nameof(Mod.ValidForFreestyleAsRequiredMod)} but does not exist in all four basic rulesets!");
|
||||
if (!mod.ValidForFreestyleAsRequiredMod && mod.UserPlayable && commonAcronyms.Contains(mod.Acronym))
|
||||
Assert.Fail($"{mod.GetType().ReadableName()} does not declare {nameof(Mod.ValidForFreestyleAsRequiredMod)} but exists in all four basic rulesets!");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
|
||||
@@ -385,7 +362,7 @@ namespace osu.Game.Tests.Mods
|
||||
{
|
||||
}
|
||||
|
||||
public class InvalidMultiplayerMod : Mod
|
||||
private class InvalidMultiplayerMod : Mod
|
||||
{
|
||||
public override string Name => string.Empty;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
@@ -406,18 +383,22 @@ namespace osu.Game.Tests.Mods
|
||||
public override bool ValidForMultiplayerAsFreeMod => false;
|
||||
}
|
||||
|
||||
public class EditableMod : Mod
|
||||
public class InvalidFreestyleRequiredMod : Mod
|
||||
{
|
||||
public override string Name => string.Empty;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override string Acronym => string.Empty;
|
||||
public override double ScoreMultiplier => Multiplier;
|
||||
|
||||
public double Multiplier = 1;
|
||||
public override bool HasImplementation => true;
|
||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||
}
|
||||
|
||||
public interface IModCompatibilitySpecification
|
||||
public interface IModCompatibilitySpecification;
|
||||
|
||||
public readonly record struct MultiplayerTestScenario(bool IsRequired, bool IsFreestyle, Mod[] Mods, Type[] InvalidTypes)
|
||||
{
|
||||
public override string ToString()
|
||||
=> $"{IsRequired}, {IsFreestyle}, [{string.Join(',', Mods.Select(m => m.GetType().ReadableName()))}], [{string.Join(',', InvalidTypes.Select(t => t.ReadableName()))}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,12 +362,14 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
new PlaylistItem(importedSet.Beatmaps[0])
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
Freestyle = true
|
||||
Freestyle = true,
|
||||
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||
},
|
||||
new PlaylistItem(importedSet.Beatmaps[0])
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
Freestyle = true
|
||||
Freestyle = true,
|
||||
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||
},
|
||||
]
|
||||
};
|
||||
@@ -452,12 +454,14 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
new PlaylistItem(importedSet.Beatmaps[0])
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
Freestyle = true
|
||||
Freestyle = true,
|
||||
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||
},
|
||||
new PlaylistItem(importedSet.Beatmaps[0])
|
||||
{
|
||||
RulesetID = new TaikoRuleset().RulesetInfo.OnlineID,
|
||||
Freestyle = true
|
||||
Freestyle = true,
|
||||
AllowedMods = [new APIMod(new TaikoModDoubleTime())]
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MessagePack;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Online.Rooms
|
||||
{
|
||||
@@ -28,9 +30,20 @@ namespace osu.Game.Online.Rooms
|
||||
[Key(4)]
|
||||
public int RulesetID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mods that should be applied for every participant in the room.
|
||||
/// </summary>
|
||||
[Key(5)]
|
||||
public IEnumerable<APIMod> RequiredMods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
/// <summary>
|
||||
/// Mods that participants are allowed to apply at their own discretion.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will be empty when <see cref="Freestyle"/> is <c>true</c>, but participants may still select any mods from their choice of ruleset,
|
||||
/// provided the mod <see cref="IMod.ValidForMultiplayerAsFreeMod">implementation</see> indicates free-mod validity
|
||||
/// and is <see cref="ModUtils.CheckCompatibleSet(IEnumerable{Mod})">compatible</see> with the rest of the user's selection.
|
||||
/// </remarks>
|
||||
[Key(6)]
|
||||
public IEnumerable<APIMod> AllowedMods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
@@ -57,7 +70,7 @@ namespace osu.Game.Online.Rooms
|
||||
public double StarRating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset.
|
||||
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty, ruleset, and mods.
|
||||
/// </summary>
|
||||
[Key(11)]
|
||||
public bool Freestyle { get; set; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
@@ -9,6 +10,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Online.Rooms
|
||||
@@ -37,9 +39,19 @@ namespace osu.Game.Online.Rooms
|
||||
[JsonProperty("played_at")]
|
||||
public DateTimeOffset? PlayedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mods that participants are allowed to apply at their own discretion.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will be empty when <see cref="Freestyle"/> is <c>true</c>, but participants may still select any mods from their choice of ruleset,
|
||||
/// provided the mod is <see cref="ModUtils.CheckCompatibleSet(IEnumerable{Mod})">compatible</see> with the rest of the user's selection.
|
||||
/// </remarks>
|
||||
[JsonProperty("allowed_mods")]
|
||||
public APIMod[] AllowedMods { get; set; } = Array.Empty<APIMod>();
|
||||
|
||||
/// <summary>
|
||||
/// Mods that should be applied for every participant in the room.
|
||||
/// </summary>
|
||||
[JsonProperty("required_mods")]
|
||||
public APIMod[] RequiredMods { get; set; } = Array.Empty<APIMod>();
|
||||
|
||||
@@ -68,7 +80,7 @@ namespace osu.Game.Online.Rooms
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty and ruleset.
|
||||
/// Indicates whether participants in the room are able to pick their own choice of beatmap difficulty, ruleset, and mods.
|
||||
/// </summary>
|
||||
[JsonProperty("freestyle")]
|
||||
public bool Freestyle { get; set; }
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
IconUsage? Icon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mod is playable by an end user.
|
||||
/// Whether this mod is playable by a real human user.
|
||||
/// Should be <c>false</c> for cases where the user is not interacting with the game (so it can be excluded from multiplayer selection, for example).
|
||||
/// </summary>
|
||||
bool UserPlayable { get; }
|
||||
@@ -53,6 +53,12 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// </summary>
|
||||
bool ValidForMultiplayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mod is valid as a required mod when freestyle is enabled.
|
||||
/// Should be <c>true</c> for mods that are guaranteed to be implemented across all rulesets.
|
||||
/// </summary>
|
||||
bool ValidForFreestyleAsRequiredMod { 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"/>).
|
||||
|
||||
@@ -87,56 +87,17 @@ namespace osu.Game.Rulesets.Mods
|
||||
[JsonIgnore]
|
||||
public virtual bool HasImplementation => this is IApplicableMod;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mod can be played by a real human user.
|
||||
/// Non-user-playable mods are not viable for single-player score submission.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="ModDoubleTime"/> is user-playable.</item>
|
||||
/// <item><see cref="ModAutoplay"/> is not user-playable.</item>
|
||||
/// </list>
|
||||
/// </example>
|
||||
[JsonIgnore]
|
||||
public virtual bool UserPlayable => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mod can be specified as a "required" mod in a multiplayer context.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="ModHardRock"/> is valid for multiplayer.</item>
|
||||
/// <item>
|
||||
/// <see cref="ModDoubleTime"/> is valid for multiplayer as long as it is a <b>required</b> mod,
|
||||
/// as that ensures the same duration of gameplay for all users in the room.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="ModAdaptiveSpeed"/> is not valid for multiplayer, as it leads to varying
|
||||
/// gameplay duration depending on how the users in the room play.
|
||||
/// </item>
|
||||
/// <item><see cref="ModAutoplay"/> is not valid for multiplayer.</item>
|
||||
/// </list>
|
||||
/// </example>
|
||||
[JsonIgnore]
|
||||
public virtual bool ValidForMultiplayer => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mod can be specified as a "free" or "allowed" mod in a multiplayer context.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="ModHardRock"/> is valid for multiplayer as a free mod.</item>
|
||||
/// <item>
|
||||
/// <see cref="ModDoubleTime"/> is <b>not</b> valid for multiplayer as a free mod,
|
||||
/// as it could to varying gameplay duration between users in the room depending on whether they picked it.
|
||||
/// </item>
|
||||
/// <item><see cref="ModAutoplay"/> is not valid for multiplayer as a free mod.</item>
|
||||
/// </list>
|
||||
/// </example>
|
||||
public virtual bool ValidForFreestyleAsRequiredMod => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public virtual bool ValidForMultiplayerAsFreeMod => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public virtual bool AlwaysValidForSubmission => false;
|
||||
|
||||
@@ -146,9 +107,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
[JsonIgnore]
|
||||
public virtual bool RequiresConfiguration => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether scores with this mod active can give performance points.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public virtual bool Ranked => false;
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override bool Ranked => true;
|
||||
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// - Sliders always gives combo for slider end, even on miss (https://github.com/ppy/osu/issues/11769).
|
||||
/// </summary>
|
||||
public sealed override bool Ranked => false;
|
||||
|
||||
public sealed override bool ValidForFreestyleAsRequiredMod => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override bool RequiresConfiguration => true;
|
||||
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModHardRock) };
|
||||
|
||||
protected const int FIRST_SETTING_ORDER = 1;
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) };
|
||||
public override bool Ranked => UsesDefaultConfiguration;
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
public virtual void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override LocalisableString Description => "Restricted view area.";
|
||||
public override bool Ranked => UsesDefaultConfiguration;
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
|
||||
public abstract BindableFloat SizeMultiplier { get; }
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override LocalisableString Description => "Everything just got a bit harder...";
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) };
|
||||
public override bool Ranked => UsesDefaultConfiguration;
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
protected const float ADJUST_RATIO = 1.4f;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModHidden;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override bool Ranked => UsesDefaultConfiguration;
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
public virtual void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => true;
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
}
|
||||
|
||||
public abstract class ModMuted<TObject> : ModMuted, IApplicableToDrawableRuleset<TObject>, IApplicableToTrack, IApplicableToScoreProcessor
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModFailCondition), typeof(ModCinema) };
|
||||
public override bool Ranked => UsesDefaultConfiguration;
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
private readonly Bindable<bool> showHealthBar = new Bindable<bool>();
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override LocalisableString Description => "SS or quit.";
|
||||
public override bool Ranked => true;
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModRateAdjust : Mod, IApplicableToRate
|
||||
{
|
||||
public sealed override bool ValidForFreestyleAsRequiredMod => true;
|
||||
public sealed override bool ValidForMultiplayerAsFreeMod => false;
|
||||
|
||||
public abstract BindableNumber<double> SpeedChange { get; }
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override LocalisableString Description => "Miss and fail.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => true;
|
||||
public override bool ValidForFreestyleAsRequiredMod => true;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray();
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||
public abstract BindableBool AdjustPitch { get; }
|
||||
|
||||
public sealed override bool ValidForFreestyleAsRequiredMod => true;
|
||||
public sealed override bool ValidForMultiplayerAsFreeMod => false;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };
|
||||
|
||||
@@ -81,14 +81,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
|
||||
MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem;
|
||||
Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance();
|
||||
Mod[] allowedMods = currentItem.Freestyle
|
||||
? ruleset.AllMods.OfType<Mod>().Where(m => ModUtils.IsValidFreeModForMatchType(m, client.Room.Settings.MatchType)).ToArray()
|
||||
: currentItem.AllowedMods.Select(m => m.ToMod(ruleset)).ToArray();
|
||||
Mod[] allowedMods = ModUtils.EnumerateUserSelectableFreeMods(client.Room.Settings.MatchType, currentItem.RequiredMods, currentItem.AllowedMods, currentItem.Freestyle, ruleset);
|
||||
|
||||
// Update the mod panels to reflect the ones which are valid for selection.
|
||||
IsValidMod = allowedMods.Length > 0
|
||||
? m => allowedMods.Any(a => a.GetType() == m.GetType())
|
||||
: _ => false;
|
||||
IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
|
||||
|
||||
// Remove any mods that are no longer allowed.
|
||||
Mod[] newUserMods = SelectedMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
freeModSelect = new FreeModSelectOverlay
|
||||
{
|
||||
SelectedMods = { BindTarget = FreeMods },
|
||||
IsValidMod = isValidFreeMod,
|
||||
IsValidMod = isValidAllowedMod,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,45 +115,74 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
Freestyle.Value = initialItem.Freestyle;
|
||||
}
|
||||
|
||||
Mods.BindValueChanged(onModsChanged);
|
||||
Mods.BindValueChanged(onGlobalModsChanged);
|
||||
Ruleset.BindValueChanged(onRulesetChanged);
|
||||
Freestyle.BindValueChanged(onFreestyleChanged, true);
|
||||
Freestyle.BindValueChanged(onFreestyleChanged);
|
||||
|
||||
freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelect);
|
||||
|
||||
updateFooterButtons();
|
||||
updateValidMods();
|
||||
}
|
||||
|
||||
private void onFreestyleChanged(ValueChangedEvent<bool> enabled)
|
||||
{
|
||||
updateFooterButtons();
|
||||
updateValidMods();
|
||||
|
||||
if (enabled.NewValue)
|
||||
{
|
||||
freeModsFooterButton.Enabled.Value = false;
|
||||
freeModsFooterButton.Enabled.Value = false;
|
||||
ModsFooterButton.Enabled.Value = false;
|
||||
|
||||
ModSelect.Hide();
|
||||
freeModSelect.Hide();
|
||||
|
||||
Mods.Value = [];
|
||||
// Freestyle allows all mods to be selected as freemods. This does not play nicely for some components:
|
||||
// - We probably don't want to store a gigantic list of acronyms to the database.
|
||||
// - The mod select overlay isn't built to handle duplicate mods/mods from all rulesets being shoved into it.
|
||||
// Instead, freestyle inherently assumes this list is empty, and must be empty for server-side validation to pass.
|
||||
FreeMods.Value = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
freeModsFooterButton.Enabled.Value = true;
|
||||
ModsFooterButton.Enabled.Value = true;
|
||||
// When disabling freestyle, enable freemods by default.
|
||||
FreeMods.Value = freeModSelect.AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void onModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
private void onGlobalModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
FreeMods.Value = FreeMods.Value.Where(isValidFreeMod).ToList();
|
||||
|
||||
// Reset the validity delegate to update the overlay's display.
|
||||
freeModSelect.IsValidMod = isValidFreeMod;
|
||||
updateValidMods();
|
||||
}
|
||||
|
||||
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
|
||||
{
|
||||
FreeMods.Value = Array.Empty<Mod>();
|
||||
// Todo: We can probably attempt to preserve across rulesets like the global mods do.
|
||||
FreeMods.Value = [];
|
||||
}
|
||||
|
||||
private void updateFooterButtons()
|
||||
{
|
||||
if (Freestyle.Value)
|
||||
{
|
||||
freeModsFooterButton.Enabled.Value = false;
|
||||
freeModSelect.Hide();
|
||||
}
|
||||
else
|
||||
freeModsFooterButton.Enabled.Value = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes invalid mods from <see cref="OsuScreen.Mods"/> and <see cref="FreeMods"/>,
|
||||
/// and updates mod selection overlays to display the new mods valid for selection.
|
||||
/// </summary>
|
||||
private void updateValidMods()
|
||||
{
|
||||
Mod[] validMods = Mods.Value.Where(isValidRequiredMod).ToArray();
|
||||
if (!validMods.SequenceEqual(Mods.Value))
|
||||
Mods.Value = validMods;
|
||||
|
||||
Mod[] validFreeMods = FreeMods.Value.Where(isValidAllowedMod).ToArray();
|
||||
if (!validFreeMods.SequenceEqual(FreeMods.Value))
|
||||
FreeMods.Value = validFreeMods;
|
||||
|
||||
ModSelect.IsValidMod = isValidRequiredMod;
|
||||
freeModSelect.IsValidMod = isValidAllowedMod;
|
||||
}
|
||||
|
||||
protected sealed override bool OnStart()
|
||||
@@ -195,7 +224,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum)
|
||||
{
|
||||
IsValidMod = isValidMod
|
||||
IsValidMod = isValidRequiredMod
|
||||
};
|
||||
|
||||
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
|
||||
@@ -221,22 +250,20 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a given <see cref="Mod"/> is valid for global selection.
|
||||
/// Checks whether a given <see cref="Mod"/> is valid to be selected as a required mod.
|
||||
/// </summary>
|
||||
/// <param name="mod">The <see cref="Mod"/> to check.</param>
|
||||
/// <returns>Whether <paramref name="mod"/> is a valid mod for online play.</returns>
|
||||
private bool isValidMod(Mod mod) => ModUtils.IsValidModForMatchType(mod, room.Type);
|
||||
private bool isValidRequiredMod(Mod mod) => ModUtils.IsValidModForMatch(mod, true, room.Type, Freestyle.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a given <see cref="Mod"/> is valid for per-player free-mod selection.
|
||||
/// Checks whether a given <see cref="Mod"/> is valid to be selected as an allowed mod.
|
||||
/// </summary>
|
||||
/// <param name="mod">The <see cref="Mod"/> to check.</param>
|
||||
/// <returns>Whether <paramref name="mod"/> is a selectable free-mod.</returns>
|
||||
private bool isValidFreeMod(Mod mod) => ModUtils.IsValidFreeModForMatchType(mod, room.Type)
|
||||
// Mod must not be contained in the required mods.
|
||||
&& Mods.Value.All(m => m.Acronym != mod.Acronym)
|
||||
// Mod must be compatible with all the required mods.
|
||||
&& ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray());
|
||||
private bool isValidAllowedMod(Mod mod) => ModUtils.IsValidModForMatch(mod, false, room.Type, Freestyle.Value)
|
||||
// Mod must not be contained in the required mods.
|
||||
&& Mods.Value.All(m => m.Acronym != mod.Acronym)
|
||||
// Mod must be compatible with all the required mods.
|
||||
&& ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray());
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
|
||||
@@ -572,31 +572,18 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
updateGameplayState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists the <see cref="Mod"/>s that are valid to be selected for the user mod style.
|
||||
/// </summary>
|
||||
private Mod[] listAllowedMods()
|
||||
{
|
||||
if (SelectedItem.Value == null)
|
||||
return [];
|
||||
|
||||
PlaylistItem item = SelectedItem.Value;
|
||||
|
||||
RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!;
|
||||
Ruleset rulesetInstance = gameplayRuleset.CreateInstance();
|
||||
|
||||
if (item.Freestyle)
|
||||
return rulesetInstance.AllMods.OfType<Mod>().Where(m => ModUtils.IsValidFreeModForMatchType(m, room.Type)).ToArray();
|
||||
|
||||
return item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the user mod style against the selected item and ruleset style.
|
||||
/// </summary>
|
||||
private void validateUserMods()
|
||||
{
|
||||
Mod[] allowedMods = listAllowedMods();
|
||||
if (SelectedItem.Value == null)
|
||||
return;
|
||||
|
||||
PlaylistItem item = SelectedItem.Value;
|
||||
RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!;
|
||||
Mod[] allowedMods = ModUtils.EnumerateUserSelectableFreeMods(MatchType.Playlists, item.RequiredMods, item.AllowedMods, item.Freestyle, gameplayRuleset.CreateInstance());
|
||||
|
||||
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
|
||||
}
|
||||
|
||||
@@ -613,7 +600,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap;
|
||||
RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!;
|
||||
Ruleset rulesetInstance = gameplayRuleset.CreateInstance();
|
||||
Mod[] allowedMods = listAllowedMods();
|
||||
Mod[] allowedMods = ModUtils.EnumerateUserSelectableFreeMods(MatchType.Playlists, item.RequiredMods, item.AllowedMods, item.Freestyle, gameplayRuleset.CreateInstance());
|
||||
|
||||
// Update global gameplay state to correspond to the new selection.
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
@@ -623,7 +610,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray();
|
||||
|
||||
// Update UI elements to reflect the new selection.
|
||||
bool freemods = allowedMods.Length > 0;
|
||||
bool freemods = item.Freestyle || allowedMods.Length > 0;
|
||||
bool freestyle = item.Freestyle;
|
||||
|
||||
if (freemods)
|
||||
|
||||
+41
-23
@@ -128,12 +128,13 @@ namespace osu.Game.Utils
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks that all <see cref="Mod"/>s in a combination are valid as "required mods" in a multiplayer match session.
|
||||
/// Checks whether the given combination of mods may be set as the <see cref="MultiplayerPlaylistItem.RequiredMods">required mods</see> of a multiplayer playlist item.
|
||||
/// </summary>
|
||||
/// <param name="mods">The mods to check.</param>
|
||||
/// <param name="freestyle">Whether freestyle is enabled for the playlist item.</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)
|
||||
public static bool CheckValidRequiredModsForMultiplayer(IEnumerable<Mod> mods, bool freestyle, [NotNullWhen(false)] out List<Mod>? invalidMods)
|
||||
{
|
||||
mods = mods.ToArray();
|
||||
|
||||
@@ -145,11 +146,11 @@ namespace osu.Game.Utils
|
||||
if (!CheckCompatibleSet(mods, out invalidMods))
|
||||
return false;
|
||||
|
||||
return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayer, out invalidMods);
|
||||
return checkValid(mods, m => IsValidModForMatch(m, true, MatchType.HeadToHead, freestyle), out invalidMods);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks that all <see cref="Mod"/>s in a combination are valid as "free mods" in a multiplayer match session.
|
||||
/// Checks whether the given mods are valid to appear as <see cref="MultiplayerPlaylistItem.AllowedMods">allowed mods</see> in a multiplayer playlist item.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this does not check compatibility between mods,
|
||||
@@ -157,10 +158,11 @@ namespace osu.Game.Utils
|
||||
/// 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="freestyle">Whether freestyle is enabled for the playlist item.</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);
|
||||
public static bool CheckValidAllowedModsForMultiplayer(IEnumerable<Mod> mods, bool freestyle, [NotNullWhen(false)] out List<Mod>? invalidMods)
|
||||
=> checkValid(mods, m => IsValidModForMatch(m, false, MatchType.HeadToHead, freestyle), out invalidMods);
|
||||
|
||||
private static bool checkValid(IEnumerable<Mod> mods, Predicate<Mod> valid, [NotNullWhen(false)] out List<Mod>? invalidMods)
|
||||
{
|
||||
@@ -295,43 +297,59 @@ namespace osu.Game.Utils
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a mod can be applied to playlist items in the given match type.
|
||||
/// Determines whether a given mod is valid on a playlist item.
|
||||
/// </summary>
|
||||
/// <param name="mod">The mod to test.</param>
|
||||
/// <param name="type">The match type.</param>
|
||||
public static bool IsValidModForMatchType(Mod mod, MatchType type)
|
||||
/// <param name="required">
|
||||
/// <c>true</c> if the mod is intended as a <see cref="MultiplayerPlaylistItem.RequiredMods">required mod</see> on the target playlist item.
|
||||
/// <c>false</c> if it is intended as an <see cref="MultiplayerPlaylistItem.AllowedMods">allowed mod</see>.
|
||||
/// </param>
|
||||
/// <param name="matchType">The type of match being played.</param>
|
||||
/// <param name="freestyle">Whether the target playlist item enables <see cref="MultiplayerPlaylistItem.Freestyle">freestyle</see> mode.</param>
|
||||
/// <seealso href="https://github.com/ppy/osu-web/blob/40936b514c6485b874f6c6496d55d9e8b1b88fd4/app/Singletons/Mods.php#L95-L113">Related osu!web function.</seealso>
|
||||
public static bool IsValidModForMatch(Mod mod, bool required, MatchType matchType, bool freestyle)
|
||||
{
|
||||
if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation)
|
||||
return false;
|
||||
|
||||
switch (type)
|
||||
if (freestyle && required && !mod.ValidForFreestyleAsRequiredMod)
|
||||
return false;
|
||||
|
||||
switch (matchType)
|
||||
{
|
||||
case MatchType.Playlists:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return mod.ValidForMultiplayer;
|
||||
return required ? mod.ValidForMultiplayer : mod.ValidForMultiplayerAsFreeMod;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a mod can be applied as a free mod to playlist items in the given match type.
|
||||
/// Given an online listing of mods and the user's preferred ruleset, gathers the mods which are selectable as free mods by the current user.
|
||||
/// </summary>
|
||||
/// <param name="mod">The mod to test.</param>
|
||||
/// <param name="type">The match type.</param>
|
||||
public static bool IsValidFreeModForMatchType(Mod mod, MatchType type)
|
||||
/// <param name="matchType">The type of match being played.</param>
|
||||
/// <param name="requiredMods">The required mods for the playlist item.</param>
|
||||
/// <param name="allowedMods">The allowed mods for the playlist item.</param>
|
||||
/// <param name="freestyle">Whether freestyle is enabled for the playlist item.</param>
|
||||
/// <param name="userRuleset">The user's preferred ruleset, which may differ from the playlist item's selection on freestyle playlist items.</param>
|
||||
public static Mod[] EnumerateUserSelectableFreeMods(MatchType matchType, IEnumerable<APIMod> requiredMods, IEnumerable<APIMod> allowedMods, bool freestyle, Ruleset userRuleset)
|
||||
{
|
||||
if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation)
|
||||
return false;
|
||||
|
||||
switch (type)
|
||||
if (freestyle)
|
||||
{
|
||||
case MatchType.Playlists:
|
||||
return true;
|
||||
Mod[] rulesetRequiredMods = requiredMods.Select(m => m.ToMod(userRuleset)).ToArray();
|
||||
|
||||
default:
|
||||
return mod.ValidForMultiplayerAsFreeMod;
|
||||
// In freestyle, the playlist item doesn't provide the allowed mods. Instead, all mods are unconditionally allowed by default.
|
||||
return userRuleset.AllMods.OfType<Mod>()
|
||||
// But the mods must still be compatible with the room...
|
||||
.Where(m => IsValidModForMatch(m, false, matchType, true))
|
||||
// ... And compatible with the required mods listing (this also handles de-duplication).
|
||||
.Where(m => CheckCompatibleSet(rulesetRequiredMods.Append(m)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
// Without freestyle, only the mods specified by the playlist item are valid.
|
||||
return allowedMods.Select(m => m.ToMod(userRuleset)).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user