1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-22 08:19:05 +08:00

Merge pull request #31850 from smoogipoo/freestyle-mods

Allow user mods in multiplayer freestyle
This commit is contained in:
Dean Herbert 2025-02-13 02:25:37 +09:00 committed by GitHub
commit ecc12abe02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 299 additions and 193 deletions

View File

@ -6,6 +6,7 @@ using System.Linq;
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;
@ -342,6 +343,40 @@ namespace osu.Game.Tests.Mods
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
{
}

View File

@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Y = -ScreenFooter.HEIGHT,
Current = { BindTarget = freeModSelectOverlay.SelectedMods },
FreeMods = { BindTarget = freeModSelectOverlay.SelectedMods },
},
footer = new ScreenFooter(),
},

View File

@ -12,11 +12,14 @@ using osu.Framework.Utils;
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;
@ -393,6 +396,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
[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;

View File

@ -266,7 +266,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
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>()

View File

@ -53,13 +53,11 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
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[]
{

View File

@ -11,31 +11,22 @@ using osu.Framework.Graphics;
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."); }
@ -104,7 +95,8 @@ namespace osu.Game.Screens.OnlinePlay
{
base.LoadComplete();
Current.BindValueChanged(_ => updateModDisplay(), true);
Freestyle.BindValueChanged(_ => updateModDisplay());
FreeMods.BindValueChanged(_ => updateModDisplay(), true);
}
/// <summary>
@ -114,16 +106,16 @@ namespace osu.Game.Screens.OnlinePlay
{
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);

View File

@ -8,23 +8,18 @@ using osu.Framework.Extensions.Color4Extensions;
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."); }
@ -37,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay
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]
@ -81,12 +76,12 @@ namespace osu.Game.Screens.OnlinePlay
{
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);

View File

@ -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;
@ -440,11 +439,14 @@ namespace osu.Game.Screens.OnlinePlay.Match
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;
@ -455,14 +457,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
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());
@ -474,7 +469,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
UserModsSelectOverlay.IsValidMod = _ => false;
}
if (freestyle)
if (item.Freestyle)
{
UserStyleSection.Show();
@ -487,7 +482,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
UserStyleDisplayContainer.Child = new DrawableRoomPlaylistItem(gameplayItem, true)
{
AllowReordering = false,
AllowEditing = freestyle,
AllowEditing = true,
RequestEdit = _ => OpenStyleSelection()
};
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
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);
}
}
}

View File

@ -11,7 +11,6 @@ using osu.Framework.Screens;
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
@ -122,9 +121,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
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;
}
}

View File

@ -98,7 +98,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
new Drawable?[]
{
// Participants column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -118,15 +117,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
}
},
// Spacer
null,
// Beatmap column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 5),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[] { new OverlinedHeader("Beatmap") },
new Drawable[] { new OverlinedHeader("Beatmap queue") },
new Drawable[]
{
addItemButton = new AddItemButton
@ -147,80 +153,67 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
SelectedItem = SelectedItem
}
},
new Drawable[]
new[]
{
new Container
UserModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Children = new[]
Alpha = 0,
Children = new Drawable[]
{
UserModsSection = new FillFlowContainer
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
new UserModSelectButton
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Height = 30,
Text = "Select",
Action = ShowUserModSelect,
},
}
},
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
new ModDisplay
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
},
}
}
},
new[]
{
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
},
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 5),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}
},
// Spacer
null,
// Main right column
new GridContainer
{
RelativeSizeAxes = Axes.Both,

View File

@ -161,11 +161,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Right = 70 },
Spacing = new Vector2(2),
Children = new Drawable[]
{
userStyleDisplay = new StyleDisplayIcon(),
userStyleDisplay = new StyleDisplayIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
userModsDisplay = new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.5f),
ExpansionMode = ExpansionMode.AlwaysContracted,
}
@ -209,15 +216,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem();
Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
if (client.Room.GetCurrentItem() is MultiplayerPlaylistItem currentItem)
{
int userBeatmapId = User.BeatmapId ?? currentItem.BeatmapID;
int userRulesetId = User.RulesetId ?? currentItem.RulesetID;
Ruleset? userRuleset = rulesets.GetRuleset(userRulesetId)?.CreateInstance();
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
int? currentModeRank = userRuleset == null ? null : User.User?.RulesetsStatistics?.GetValueOrDefault(userRuleset.ShortName)?.GlobalRank;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
if (userBeatmapId == currentItem.BeatmapID && userRulesetId == currentItem.RulesetID)
userStyleDisplay.Style = null;
else
userStyleDisplay.Style = (userBeatmapId, userRulesetId);
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
Schedule(() => userModsDisplay.Current.Value = userRuleset == null ? Array.Empty<Mod>() : User.Mods.Select(m => m.ToMod(userRuleset)).ToList());
}
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating))
if (User.BeatmapAvailability.State == DownloadState.LocallyAvailable && User.State != MultiplayerUserState.Spectating)
{
userModsDisplay.FadeIn(fade_time);
userStyleDisplay.FadeIn(fade_time);
@ -228,20 +248,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
userStyleDisplay.FadeOut(fade_time);
}
if ((User.BeatmapId == null && User.RulesetId == null) || (User.BeatmapId == currentItem?.BeatmapID && User.RulesetId == currentItem?.RulesetID))
userStyleDisplay.Style = null;
else
userStyleDisplay.Style = (User.BeatmapId ?? currentItem?.BeatmapID ?? 0, User.RulesetId ?? currentItem?.RulesetID ?? 0);
kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0;
crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0;
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
Schedule(() =>
{
userModsDisplay.Current.Value = ruleset != null ? User.Mods.Select(m => m.ToMod(ruleset)).ToList() : Array.Empty<Mod>();
});
}
public MenuItem[]? ContextMenuItems

View File

@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay
protected override UserActivity InitialActivity => new UserActivity.InLobby(room);
protected readonly Bindable<IReadOnlyList<Mod>> FreeMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
protected readonly Bindable<bool> Freestyle = new Bindable<bool>();
protected readonly Bindable<bool> Freestyle = new Bindable<bool>(true);
private readonly Room room;
private readonly PlaylistItem? initialItem;
@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay
freeModSelect = new FreeModSelectOverlay
{
SelectedMods = { BindTarget = FreeMods },
IsValidMod = IsValidFreeMod,
IsValidMod = isValidFreeMod,
};
}
@ -126,6 +126,7 @@ namespace osu.Game.Screens.OnlinePlay
{
if (enabled.NewValue)
{
freeModsFooterButton.Enabled.Value = false;
freeModsFooterButton.Enabled.Value = false;
ModsFooterButton.Enabled.Value = false;
@ -144,10 +145,10 @@ namespace osu.Game.Screens.OnlinePlay
private void onModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList();
FreeMods.Value = FreeMods.Value.Where(isValidFreeMod).ToList();
// Reset the validity delegate to update the overlay's display.
freeModSelect.IsValidMod = IsValidFreeMod;
freeModSelect.IsValidMod = isValidFreeMod;
}
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> ruleset)
@ -194,7 +195,7 @@ namespace osu.Game.Screens.OnlinePlay
protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(OverlayColourScheme.Plum)
{
IsValidMod = IsValidMod
IsValidMod = isValidMod
};
protected override IEnumerable<(FooterButton button, OverlayContainer? overlay)> CreateSongSelectFooterButtons()
@ -205,8 +206,15 @@ namespace osu.Game.Screens.OnlinePlay
baseButtons.InsertRange(baseButtons.FindIndex(b => b.button is FooterButtonMods) + 1, new (FooterButton, OverlayContainer?)[]
{
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect) { Current = FreeMods }, null),
(new FooterButtonFreestyle { Current = Freestyle }, null)
(freeModsFooterButton = new FooterButtonFreeMods(freeModSelect)
{
FreeMods = { BindTarget = FreeMods },
Freestyle = { BindTarget = Freestyle }
}, null),
(new FooterButtonFreestyle
{
Freestyle = { BindTarget = Freestyle }
}, null)
});
return baseButtons;
@ -217,18 +225,18 @@ namespace osu.Game.Screens.OnlinePlay
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
/// <returns>Whether <paramref name="mod"/> is a valid mod for online play.</returns>
protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && ModUtils.FlattenMod(mod).All(m => m.UserPlayable);
private bool isValidMod(Mod mod) => ModUtils.IsValidModForMatchType(mod, room.Type);
/// <summary>
/// Checks whether a given <see cref="Mod"/> is valid for per-player free-mod selection.
/// </summary>
/// <param name="mod">The <see cref="Mod"/> to check.</param>
/// <returns>Whether <paramref name="mod"/> is a selectable free-mod.</returns>
protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod);
private bool checkCompatibleFreeMod(Mod mod)
=> Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods.
&& ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods.
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());
protected override void Dispose(bool isDisposing)
{

View File

@ -146,7 +146,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
new Drawable?[]
{
// Playlist items column
new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -176,73 +175,67 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Dimension(),
}
},
// Spacer
null,
// Middle column (mods and leaderboard)
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
new[]
{
new Container
UserModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 10 },
Children = new[]
Alpha = 0,
Children = new Drawable[]
{
UserModsSection = new FillFlowContainer
new OverlinedHeader("Extra mods"),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Margin = new MarginPadding { Bottom = 10 },
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new OverlinedHeader("Extra mods"),
new FillFlowContainer
new UserModSelectButton
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new UserModSelectButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = 90,
Height = 30,
Text = "Select",
Action = ShowUserModSelect,
},
new ModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = UserMods,
Scale = new Vector2(0.8f),
},
}
},
UserStyleSection = new FillFlowContainer
}
}
},
},
new[]
{
UserStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 10 },
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
UserStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
},
AutoSizeAxes = Axes.Y
}
}
},
},
@ -273,12 +266,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
},
// Spacer
null,
// Main right column
new GridContainer
{
RelativeSizeAxes = Axes.Both,

View File

@ -25,6 +25,11 @@ namespace osu.Game.Screens.Select
protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0);
/// <summary>
/// Used to show an initial animation hinting at the enabled state.
/// </summary>
protected virtual bool IsActive => false;
public LocalisableString Text
{
get => SpriteText?.Text ?? default;
@ -124,6 +129,18 @@ namespace osu.Game.Screens.Select
{
base.LoadComplete();
Enabled.BindValueChanged(_ => updateDisplay(), true);
if (IsActive)
{
box.ClearTransforms();
using (box.BeginDelayedSequence(200))
{
box.FadeIn(200)
.Then()
.FadeOut(1500, Easing.OutQuint);
}
}
}
public Action Hovered;

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -292,5 +293,45 @@ namespace osu.Game.Utils
return rate;
}
/// <summary>
/// Determines whether a mod can be applied to playlist items in the given match type.
/// </summary>
/// <param name="mod">The mod to test.</param>
/// <param name="type">The match type.</param>
public static bool IsValidModForMatchType(Mod mod, MatchType type)
{
if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation)
return false;
switch (type)
{
case MatchType.Playlists:
return true;
default:
return mod.ValidForMultiplayer;
}
}
/// <summary>
/// Determines whether a mod can be applied as a free mod to playlist items in the given match type.
/// </summary>
/// <param name="mod">The mod to test.</param>
/// <param name="type">The match type.</param>
public static bool IsValidFreeModForMatchType(Mod mod, MatchType type)
{
if (mod.Type == ModType.System || !mod.UserPlayable || !mod.HasImplementation)
return false;
switch (type)
{
case MatchType.Playlists:
return true;
default:
return mod.ValidForMultiplayerAsFreeMod;
}
}
}
}