Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow user mods in multiplayer freestyle #31850

Merged
merged 14 commits into from
Feb 12, 2025
Merged
35 changes: 35 additions & 0 deletions osu.Game.Tests/Mods/ModUtilsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Moq;
using NUnit.Framework;
using osu.Framework.Localisation;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
Expand Down Expand Up @@ -342,6 +343,40 @@ public void TestFormatScoreMultiplier()
Assert.AreEqual(ModUtils.FormatScoreMultiplier(1.055).ToString(), "1.06x");
}

[Test]
public void TestRoomModValidity()
{
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));

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));
}

[Test]
public void TestRoomFreeModValidity()
{
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));

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));
}

public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private void createFreeModSelect()
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Y = -ScreenFooter.HEIGHT,
Current = { BindTarget = freeModSelectOverlay.SelectedMods },
FreeMods = { BindTarget = freeModSelectOverlay.SelectedMods },
},
footer = new ScreenFooter(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Users;
using osuTK;
Expand Down Expand Up @@ -393,6 +396,40 @@ public void TestModOverlap()
});
}

[Test]
public void TestModsAndRuleset()
{
AddStep("add another user", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = 0,
Username = "User 0",
RulesetsStatistics = new Dictionary<string, UserStatistics>
{
{
Ruleset.Value.ShortName,
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
}
},
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});

MultiplayerClient.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable());
});

AddStep("set user styles", () =>
{
MultiplayerClient.ChangeUserStyle(API.LocalUser.Value.OnlineID, 259, 1);
MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID,
[new APIMod(new TaikoModConstantSpeed()), new APIMod(new TaikoModHidden()), new APIMod(new TaikoModFlashlight()), new APIMod(new TaikoModHardRock())]);

MultiplayerClient.ChangeUserStyle(0, 259, 2);
MultiplayerClient.ChangeUserMods(0,
[new APIMod(new CatchModFloatingFruits()), new APIMod(new CatchModHidden()), new APIMod(new CatchModMirror())]);
});
}

private void createNewParticipantsList()
{
ParticipantsList? participantsList = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ private void assertItemInHistoryListStep(int playlistItemId, int visualIndex)

private void assertQueueTabCount(int count)
{
string queueTabText = count > 0 ? $"Queue ({count})" : "Queue";
string queueTabText = count > 0 ? $"Up next ({count})" : "Up next";
AddUntilStep($"Queue tab shows \"{queueTabText}\"", () =>
{
return this.ChildrenOfType<OsuTabControl<MultiplayerPlaylistDisplayMode>.OsuTabItem>()
Expand Down
2 changes: 0 additions & 2 deletions osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,11 @@ public OverlinedHeader(LocalisableString title)
{
RelativeSizeAxes = Axes.X,
Height = 2,
Margin = new MarginPadding { Bottom = 2 }
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Top = 5 },
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
Expand Down
28 changes: 10 additions & 18 deletions osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,22 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osuTK;
using osu.Game.Localisation;

namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
public partial class FooterButtonFreeMods : FooterButton
{
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>();
public readonly IBindable<bool> Freestyle = new Bindable<bool>();

public Bindable<IReadOnlyList<Mod>> Current
{
get => current.Current;
set
{
ArgumentNullException.ThrowIfNull(value);

current.Current = value;
}
}
protected override bool IsActive => FreeMods.Value.Count > 0;

public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }

Expand Down Expand Up @@ -104,7 +95,8 @@ protected override void LoadComplete()
{
base.LoadComplete();

Current.BindValueChanged(_ => updateModDisplay(), true);
Freestyle.BindValueChanged(_ => updateModDisplay());
FreeMods.BindValueChanged(_ => updateModDisplay(), true);
}

/// <summary>
Expand All @@ -114,16 +106,16 @@ private void toggleAllFreeMods()
{
var availableMods = allAvailableAndValidMods.ToArray();

Current.Value = Current.Value.Count == availableMods.Length
FreeMods.Value = FreeMods.Value.Count == availableMods.Length
? Array.Empty<Mod>()
: availableMods;
}

private void updateModDisplay()
{
int currentCount = Current.Value.Count;
int currentCount = FreeMods.Value.Count;

if (currentCount == allAvailableAndValidMods.Count())
if (currentCount == allAvailableAndValidMods.Count() || Freestyle.Value)
{
count.Text = "all";
count.FadeColour(colours.Gray2, 200, Easing.OutQuint);
Expand Down
19 changes: 7 additions & 12 deletions osu.Game/Screens/OnlinePlay/FooterButtonFreestyle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,18 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Select;
using osu.Game.Localisation;
using osu.Game.Screens.Select;

namespace osu.Game.Screens.OnlinePlay
{
public partial class FooterButtonFreestyle : FooterButton, IHasCurrentValue<bool>
public partial class FooterButtonFreestyle : FooterButton
{
private readonly BindableWithCurrent<bool> current = new BindableWithCurrent<bool>();
public readonly Bindable<bool> Freestyle = new Bindable<bool>();

public Bindable<bool> Current
{
get => current.Current;
set => current.Current = value;
}
protected override bool IsActive => Freestyle.Value;

public new Action Action { set => throw new NotSupportedException("The click action is handled by the button itself."); }

Expand All @@ -37,7 +32,7 @@ public Bindable<bool> Current
public FooterButtonFreestyle()
{
// Overwrite any external behaviour as we delegate the main toggle action to a sub-button.
base.Action = () => current.Value = !current.Value;
base.Action = () => Freestyle.Value = !Freestyle.Value;
}

[BackgroundDependencyLoader]
Expand Down Expand Up @@ -81,12 +76,12 @@ protected override void LoadComplete()
{
base.LoadComplete();

Current.BindValueChanged(_ => updateDisplay(), true);
Freestyle.BindValueChanged(_ => updateDisplay(), true);
}

private void updateDisplay()
{
if (current.Value)
if (Freestyle.Value)
{
text.Text = "on";
text.FadeColour(colours.Gray2, 200, Easing.OutQuint);
Expand Down
21 changes: 8 additions & 13 deletions osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
Expand Down Expand Up @@ -440,11 +439,14 @@ private void updateSpecifics()

var rulesetInstance = GetGameplayRuleset().CreateInstance();

Mod[] allowedMods = item.Freestyle
? rulesetInstance.AllMods.OfType<Mod>().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray()
: item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();

// Remove any user mods that are no longer allowed.
Mod[] allowedMods = item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
if (!newUserMods.SequenceEqual(UserMods.Value))
UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList();
UserMods.Value = newUserMods;

// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
int beatmapId = GetGameplayBeatmap().OnlineID;
Expand All @@ -455,14 +457,7 @@ private void updateSpecifics()
Mods.Value = GetGameplayMods().Select(m => m.ToMod(rulesetInstance)).ToArray();
Ruleset.Value = GetGameplayRuleset();

bool freeMod = item.AllowedMods.Any();
bool freestyle = item.Freestyle;

// For now, the game can never be in a state where freemod and freestyle are on at the same time.
// This will change, but due to the current implementation if this was to occur drawables will overlap so let's assert.
Debug.Assert(!freeMod || !freestyle);

if (freeMod)
if (allowedMods.Length > 0)
{
UserModsSection.Show();
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
Expand All @@ -474,7 +469,7 @@ private void updateSpecifics()
UserModsSelectOverlay.IsValidMod = _ => false;
}

if (freestyle)
if (item.Freestyle)
{
UserStyleSection.Show();

Expand All @@ -487,7 +482,7 @@ private void updateSpecifics()
UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true)
{
AllowReordering = false,
AllowEditing = freestyle,
AllowEditing = true,
RequestEdit = _ => OpenStyleSelection()
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public QueueTabItem()
protected override void LoadComplete()
{
base.LoadComplete();
QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true);
QueueItems.BindCollectionChanged((_, _) => Text.Text = QueueItems.Count > 0 ? $"Up next ({QueueItems.Count})" : "Up next", true);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;

namespace osu.Game.Screens.OnlinePlay.Multiplayer
Expand Down Expand Up @@ -122,9 +121,5 @@ protected override bool SelectItem(PlaylistItem item)
}

protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();

protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer;

protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod;
}
}
Loading
Loading