mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 12:40:18 +08:00
Merge branch 'ruleset-specific-user-tags' into better-user-tag-ui
This commit is contained in:
@@ -43,7 +43,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -99,7 +97,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -114,7 +111,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
|
||||
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
@@ -128,7 +125,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -143,7 +139,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
|
||||
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
|
||||
@@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
[Test]
|
||||
public void TestDailyChallenge()
|
||||
{
|
||||
startChallenge(1234);
|
||||
startChallenge();
|
||||
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayIntroOnceFlag()
|
||||
{
|
||||
startChallenge(1234);
|
||||
startChallenge();
|
||||
AddStep("set intro played flag", () => Dependencies.Get<SessionStatics>().SetValue(Static.DailyChallengeIntroPlayed, true));
|
||||
|
||||
startChallenge(1235);
|
||||
startChallenge();
|
||||
|
||||
AddAssert("intro played flag reset", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.False);
|
||||
|
||||
@@ -62,13 +62,12 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
AddUntilStep("intro played flag set", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.True);
|
||||
}
|
||||
|
||||
private void startChallenge(int roomId)
|
||||
private void startChallenge()
|
||||
{
|
||||
AddStep("add room", () =>
|
||||
{
|
||||
API.Perform(new CreateRoomRequest(room = new Room
|
||||
{
|
||||
RoomID = roomId,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -83,7 +82,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
Category = RoomCategory.DailyChallenge
|
||||
}));
|
||||
});
|
||||
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }));
|
||||
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID!.Value }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
@@ -458,6 +459,62 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAnchorRadioButtonBehavior()
|
||||
{
|
||||
ISerialisableDrawable? selectedComponent = null;
|
||||
|
||||
AddStep("Select first component", () =>
|
||||
{
|
||||
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First();
|
||||
skinEditor.SelectedComponents.Clear();
|
||||
skinEditor.SelectedComponents.Add(blueprint.Item);
|
||||
selectedComponent = blueprint.Item;
|
||||
});
|
||||
|
||||
AddStep("Right-click to open context menu", () =>
|
||||
{
|
||||
if (selectedComponent != null)
|
||||
InputManager.MoveMouseTo(((Drawable)selectedComponent).ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddStep("Click on Anchor menu", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("Anchor"));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("Right-click TopLeft anchor", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("TopLeft"));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddAssert("TopLeft item checked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
|
||||
|
||||
AddStep("Right-click Centre anchor", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("Centre"));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddAssert("Centre item checked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
|
||||
AddAssert("TopLeft item unchecked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
|
||||
|
||||
AddStep("Right-click Closest anchor", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("Closest"));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddAssert("Closest item checked", () => (getMenuItemByText("Closest").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
|
||||
AddAssert("Centre item unchecked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
|
||||
|
||||
Menu.DrawableMenuItem getMenuItemByText(string text)
|
||||
=> this.ChildrenOfType<Menu.DrawableMenuItem>().First(m => m.Item.Text.ToString() == text);
|
||||
}
|
||||
|
||||
private Skin importSkinFromArchives(string filename)
|
||||
{
|
||||
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
@@ -32,7 +31,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private BeatmapManager beatmaps = null!;
|
||||
private BeatmapSetInfo importedSet = null!;
|
||||
private BeatmapInfo importedBeatmap = null!;
|
||||
private Room room = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -47,19 +45,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("create list", () =>
|
||||
{
|
||||
Child = list = new MultiplayerPlaylist(room)
|
||||
Child = list = new MultiplayerPlaylist
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.4f, 0.8f),
|
||||
SelectedItem = new Bindable<PlaylistItem?>()
|
||||
Size = new Vector2(0.4f, 0.8f)
|
||||
};
|
||||
});
|
||||
|
||||
@@ -158,37 +154,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
assertQueueTabCount(0);
|
||||
}
|
||||
|
||||
[Ignore("Expired items are initially removed from the room.")]
|
||||
[Test]
|
||||
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
|
||||
{
|
||||
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
|
||||
AddUntilStep("wait for room part", () => !RoomJoined);
|
||||
|
||||
AddStep("join room with items", () =>
|
||||
AddStep("join room with expired items", () =>
|
||||
{
|
||||
API.Queue(new CreateRoomRequest(new Room
|
||||
{
|
||||
Name = "test name",
|
||||
Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
{
|
||||
RulesetID = Ruleset.Value.OnlineID
|
||||
},
|
||||
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
{
|
||||
RulesetID = Ruleset.Value.OnlineID,
|
||||
Expired = true
|
||||
}
|
||||
]
|
||||
}));
|
||||
Room room = CreateDefaultRoom();
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
{
|
||||
RulesetID = Ruleset.Value.OnlineID
|
||||
},
|
||||
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
{
|
||||
RulesetID = Ruleset.Value.OnlineID,
|
||||
Expired = true
|
||||
}
|
||||
];
|
||||
|
||||
JoinRoom(room);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
WaitForJoined();
|
||||
|
||||
assertItemInQueueListStep(1, 0);
|
||||
assertItemInHistoryListStep(2, 0);
|
||||
// IDs are offset by 1 because we've joined two rooms in this test.
|
||||
assertItemInQueueListStep(2, 0);
|
||||
assertItemInHistoryListStep(3, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -49,13 +49,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create playlist", () =>
|
||||
{
|
||||
Child = playlist = new MultiplayerQueueList(room)
|
||||
Child = playlist = new MultiplayerQueueList
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(500, 300),
|
||||
Size = new Vector2(500, 300)
|
||||
};
|
||||
|
||||
playlist.Items.ReplaceRange(0, playlist.Items.Count, MultiplayerClient.ClientAPIRoom!.Playlist);
|
||||
|
||||
MultiplayerClient.ClientAPIRoom!.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Room.Playlist))
|
||||
@@ -132,6 +134,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
assertDeleteButtonVisibility(1, false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeExistingItem()
|
||||
{
|
||||
AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = playlist.Items[0].ID,
|
||||
BeatmapID = 1337
|
||||
}).WaitSafely());
|
||||
|
||||
AddUntilStep("first playlist item has new beatmap", () => playlist.Items[0].Beatmap.OnlineID, () => Is.EqualTo(1337));
|
||||
}
|
||||
|
||||
private void addPlaylistItem(Func<int> userId)
|
||||
{
|
||||
long itemId = -1;
|
||||
|
||||
@@ -8,6 +8,8 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
@@ -19,10 +21,6 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("set up working beatmap", () =>
|
||||
{
|
||||
Beatmap.Value.BeatmapInfo.OnlineID = 42;
|
||||
});
|
||||
AddStep("set up network requests", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = request =>
|
||||
@@ -40,6 +38,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", },
|
||||
new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", },
|
||||
new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", },
|
||||
new APITag { Id = 5, Name = "style/mono-heavy", Description = "Features monos used in large amounts.", RulesetId = 1, },
|
||||
]
|
||||
}), 500);
|
||||
return true;
|
||||
@@ -67,15 +66,30 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("create control", () =>
|
||||
AddStep("show for osu! beatmap", () =>
|
||||
{
|
||||
Child = new UserTagControl(Beatmap.Value.BeatmapInfo)
|
||||
{
|
||||
Width = 700,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
working.BeatmapInfo.OnlineID = 42;
|
||||
Beatmap.Value = working;
|
||||
recreateControl();
|
||||
});
|
||||
AddStep("show for taiko beatmap", () =>
|
||||
{
|
||||
var working = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo);
|
||||
working.BeatmapInfo.OnlineID = 44;
|
||||
Beatmap.Value = working;
|
||||
recreateControl();
|
||||
});
|
||||
}
|
||||
|
||||
private void recreateControl()
|
||||
{
|
||||
Child = new UserTagControl(Beatmap.Value.BeatmapInfo)
|
||||
{
|
||||
Width = 700,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("ruleset_id")]
|
||||
public int? RulesetId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,11 +760,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
#region Delegation of IEditorChangeHandler
|
||||
|
||||
public event Action? OnStateChange
|
||||
{
|
||||
add => throw new NotImplementedException();
|
||||
remove => throw new NotImplementedException();
|
||||
}
|
||||
public event Action? OnStateChange;
|
||||
|
||||
private IEditorChangeHandler? beginChangeHandler;
|
||||
|
||||
@@ -773,6 +769,9 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
// Change handler may change between begin and end, which can cause unbalanced operations.
|
||||
// Let's track the one that was used when beginning the change so we can call EndChange on it specifically.
|
||||
(beginChangeHandler = changeHandler)?.BeginChange();
|
||||
|
||||
if (beginChangeHandler != null)
|
||||
beginChangeHandler.OnStateChange += OnStateChange;
|
||||
}
|
||||
|
||||
public void EndChange() => beginChangeHandler?.EndChange();
|
||||
|
||||
@@ -22,7 +22,10 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class SkinSelectionHandler : SelectionHandler<ISerialisableDrawable>
|
||||
{
|
||||
private OsuMenuItem originMenu = null!;
|
||||
private OsuMenuItem? originMenu;
|
||||
|
||||
private TernaryStateRadioMenuItem? closestAnchor;
|
||||
private AnchorMenuItem[]? fixedAnchors;
|
||||
|
||||
[Resolved]
|
||||
private SkinEditor skinEditor { get; set; } = null!;
|
||||
@@ -44,6 +47,38 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
return scaleHandler;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (ChangeHandler != null)
|
||||
ChangeHandler.OnStateChange += updateTernaryStates;
|
||||
SelectedItems.BindCollectionChanged((_, _) => updateTernaryStates());
|
||||
}
|
||||
|
||||
private void updateTernaryStates()
|
||||
{
|
||||
var usingClosestAnchor = GetStateFromSelection(SelectedBlueprints, c => !c.Item.UsesFixedAnchor);
|
||||
|
||||
if (closestAnchor != null)
|
||||
closestAnchor.State.Value = usingClosestAnchor;
|
||||
|
||||
if (fixedAnchors != null)
|
||||
{
|
||||
foreach (var fixedAnchor in fixedAnchors)
|
||||
fixedAnchor.State.Value = GetStateFromSelection(SelectedBlueprints, c => c.Item.UsesFixedAnchor && ((Drawable)c.Item).Anchor == fixedAnchor.Anchor);
|
||||
}
|
||||
|
||||
if (originMenu != null)
|
||||
{
|
||||
foreach (var origin in originMenu.Items.OfType<AnchorMenuItem>())
|
||||
{
|
||||
origin.State.Value = GetStateFromSelection(SelectedBlueprints, c => ((Drawable)c.Item).Origin == origin.Anchor);
|
||||
origin.Action.Disabled = usingClosestAnchor == TernaryState.True;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool HandleFlip(Direction direction, bool flipOverOrigin)
|
||||
{
|
||||
var selectionQuad = getSelectionQuad();
|
||||
@@ -102,27 +137,17 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISerialisableDrawable>> selection)
|
||||
{
|
||||
var closestItem = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors())
|
||||
{
|
||||
State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) }
|
||||
};
|
||||
closestAnchor = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors());
|
||||
fixedAnchors = createAnchorItems(applyFixedAnchors).ToArray();
|
||||
|
||||
yield return new OsuMenuItem(SkinEditorStrings.Anchor)
|
||||
{
|
||||
Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors)
|
||||
.Prepend(closestItem)
|
||||
.ToArray()
|
||||
Items = fixedAnchors.Prepend(closestAnchor).ToArray()
|
||||
};
|
||||
|
||||
yield return originMenu = new OsuMenuItem(SkinEditorStrings.Origin);
|
||||
|
||||
closestItem.State.BindValueChanged(s =>
|
||||
{
|
||||
// For UX simplicity, origin should only be user-editable when "closest" anchor mode is disabled.
|
||||
originMenu.Items = s.NewValue == TernaryState.True
|
||||
? Array.Empty<MenuItem>()
|
||||
: createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray();
|
||||
}, true);
|
||||
originMenu.Items = createAnchorItems(applyOrigins).ToArray();
|
||||
|
||||
yield return new OsuMenuItemSpacer();
|
||||
|
||||
@@ -163,27 +188,37 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
yield return item;
|
||||
|
||||
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<ISerialisableDrawable, Anchor, bool> checkFunction, Action<Anchor> applyFunction)
|
||||
updateTernaryStates();
|
||||
}
|
||||
|
||||
private IEnumerable<AnchorMenuItem> createAnchorItems(Action<Anchor> applyFunction)
|
||||
{
|
||||
var displayableAnchors = new[]
|
||||
{
|
||||
var displayableAnchors = new[]
|
||||
{
|
||||
Anchor.TopLeft,
|
||||
Anchor.TopCentre,
|
||||
Anchor.TopRight,
|
||||
Anchor.CentreLeft,
|
||||
Anchor.Centre,
|
||||
Anchor.CentreRight,
|
||||
Anchor.BottomLeft,
|
||||
Anchor.BottomCentre,
|
||||
Anchor.BottomRight,
|
||||
};
|
||||
return displayableAnchors.Select(a =>
|
||||
{
|
||||
return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a))
|
||||
{
|
||||
State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item, a)) }
|
||||
};
|
||||
});
|
||||
Anchor.TopLeft,
|
||||
Anchor.TopCentre,
|
||||
Anchor.TopRight,
|
||||
Anchor.CentreLeft,
|
||||
Anchor.Centre,
|
||||
Anchor.CentreRight,
|
||||
Anchor.BottomLeft,
|
||||
Anchor.BottomCentre,
|
||||
Anchor.BottomRight,
|
||||
};
|
||||
return displayableAnchors.Select(a =>
|
||||
{
|
||||
return new AnchorMenuItem(a, _ => applyFunction(a));
|
||||
});
|
||||
}
|
||||
|
||||
private partial class AnchorMenuItem : TernaryStateRadioMenuItem
|
||||
{
|
||||
public readonly Anchor Anchor;
|
||||
|
||||
public AnchorMenuItem(Anchor anchor, Action<Anchor> applyFunction)
|
||||
: base(anchor.ToString(), MenuItemType.Standard, _ => applyFunction(anchor))
|
||||
{
|
||||
Anchor = anchor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
@@ -19,12 +20,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
{
|
||||
public readonly Bindable<MultiplayerPlaylistDisplayMode> DisplayMode = new Bindable<MultiplayerPlaylistDisplayMode>();
|
||||
|
||||
public required Bindable<PlaylistItem?> SelectedItem
|
||||
{
|
||||
get => selectedItem;
|
||||
set => selectedItem.Current = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when an item requests to be edited.
|
||||
/// </summary>
|
||||
@@ -33,18 +28,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private readonly Room room;
|
||||
private readonly BindableWithCurrent<PlaylistItem?> selectedItem = new BindableWithCurrent<PlaylistItem?>();
|
||||
private MultiplayerPlaylistTabControl playlistTabControl = null!;
|
||||
private MultiplayerQueueList queueList = null!;
|
||||
private MultiplayerHistoryList historyList = null!;
|
||||
private bool firstPopulation = true;
|
||||
|
||||
public MultiplayerPlaylist(Room room)
|
||||
{
|
||||
this.room = room;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@@ -65,17 +53,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
queueList = new MultiplayerQueueList(room)
|
||||
queueList = new MultiplayerQueueList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
SelectedItem = { BindTarget = selectedItem },
|
||||
RequestEdit = item => RequestEdit?.Invoke(item)
|
||||
},
|
||||
historyList = new MultiplayerHistoryList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
SelectedItem = { BindTarget = selectedItem }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,10 +75,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
base.LoadComplete();
|
||||
|
||||
DisplayMode.BindValueChanged(onDisplayModeChanged, true);
|
||||
|
||||
client.ItemAdded += playlistItemAdded;
|
||||
client.ItemRemoved += playlistItemRemoved;
|
||||
client.ItemChanged += playlistItemChanged;
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
|
||||
updateState();
|
||||
}
|
||||
|
||||
@@ -121,28 +109,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
|
||||
firstPopulation = false;
|
||||
}
|
||||
|
||||
PlaylistItem? currentItem = client.Room == null ? null : new PlaylistItem(client.Room.CurrentPlaylistItem);
|
||||
queueList.SelectedItem.Value = currentItem;
|
||||
historyList.SelectedItem.Value = currentItem;
|
||||
}
|
||||
|
||||
private void playlistItemAdded(MultiplayerPlaylistItem item) => Schedule(() => addItemToLists(item));
|
||||
private void playlistItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => addItemToLists(item));
|
||||
|
||||
private void playlistItemRemoved(long item) => Schedule(() => removeItemFromLists(item));
|
||||
private void playlistItemRemoved(long item) => Scheduler.Add(() => removeItemFromLists(item));
|
||||
|
||||
private void playlistItemChanged(MultiplayerPlaylistItem item) => Schedule(() =>
|
||||
private void playlistItemChanged(MultiplayerPlaylistItem item) => Scheduler.Add(() =>
|
||||
{
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
var newApiItem = new PlaylistItem(item);
|
||||
var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
|
||||
var existingItem = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
|
||||
|
||||
// Test if the only change between the two playlist items is the order.
|
||||
if (existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
|
||||
if (existingItem != null && existingItem.With(playlistOrder: item.PlaylistOrder).Equals(new PlaylistItem(item)))
|
||||
{
|
||||
// Set the new playlist order directly without refreshing the DrawablePlaylistItem.
|
||||
existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder;
|
||||
|
||||
// The following isn't really required, but is here for safety and explicitness.
|
||||
// MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation.
|
||||
// Set the new order directly and refresh the flow layout as an optimisation to avoid refreshing the items' visual state.
|
||||
existingItem.PlaylistOrder = item.PlaylistOrder;
|
||||
queueList.Invalidate();
|
||||
}
|
||||
else
|
||||
@@ -154,22 +142,35 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
|
||||
private void addItemToLists(MultiplayerPlaylistItem item)
|
||||
{
|
||||
var apiItem = client.Room?.Playlist.SingleOrDefault(i => i.ID == item.ID);
|
||||
|
||||
// Item could have been removed from the playlist while the local player was in gameplay.
|
||||
if (apiItem == null)
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
if (item.Expired)
|
||||
historyList.Items.Add(new PlaylistItem(apiItem));
|
||||
historyList.Items.Add(new PlaylistItem(item));
|
||||
else
|
||||
queueList.Items.Add(new PlaylistItem(apiItem));
|
||||
queueList.Items.Add(new PlaylistItem(item));
|
||||
}
|
||||
|
||||
private void removeItemFromLists(long item)
|
||||
private void removeItemFromLists(long itemId)
|
||||
{
|
||||
queueList.Items.RemoveAll(i => i.ID == item);
|
||||
historyList.Items.RemoveAll(i => i.ID == item);
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
queueList.Items.RemoveAll(i => i.ID == itemId);
|
||||
historyList.Items.RemoveAll(i => i.ID == itemId);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.ItemAdded -= playlistItemAdded;
|
||||
client.ItemRemoved -= playlistItemRemoved;
|
||||
client.ItemChanged -= playlistItemChanged;
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -20,50 +19,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
/// </summary>
|
||||
public partial class MultiplayerQueueList : DrawableRoomPlaylist
|
||||
{
|
||||
private readonly Room room;
|
||||
|
||||
private QueueFillFlowContainer flow = null!;
|
||||
|
||||
public MultiplayerQueueList(Room room)
|
||||
public MultiplayerQueueList()
|
||||
{
|
||||
this.room = room;
|
||||
ShowItemOwners = true;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
room.PropertyChanged += onRoomPropertyChanged;
|
||||
updateRoomPlaylist();
|
||||
}
|
||||
|
||||
private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(Room.Playlist))
|
||||
updateRoomPlaylist();
|
||||
}
|
||||
|
||||
private void updateRoomPlaylist()
|
||||
=> flow.InvalidateLayout();
|
||||
|
||||
protected override FillFlowContainer<RearrangeableListItem<PlaylistItem>> CreateListFillFlowContainer() => flow = new QueueFillFlowContainer
|
||||
protected override FillFlowContainer<RearrangeableListItem<PlaylistItem>> CreateListFillFlowContainer() => new QueueFillFlowContainer
|
||||
{
|
||||
Spacing = new Vector2(0, 2)
|
||||
};
|
||||
|
||||
protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
room.PropertyChanged -= onRoomPropertyChanged;
|
||||
}
|
||||
|
||||
private partial class QueueFillFlowContainer : FillFlowContainer<RearrangeableListItem<PlaylistItem>>
|
||||
{
|
||||
public new void InvalidateLayout() => base.InvalidateLayout();
|
||||
|
||||
public override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.OfType<RearrangeableListItem<PlaylistItem>>().OrderBy(item => item.Model.PlaylistOrder);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,11 +145,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
null,
|
||||
new Drawable[]
|
||||
{
|
||||
new MultiplayerPlaylist(Room)
|
||||
new MultiplayerPlaylist
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RequestEdit = OpenSongSelection,
|
||||
SelectedItem = SelectedItem
|
||||
RequestEdit = OpenSongSelection
|
||||
}
|
||||
},
|
||||
new[]
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace osu.Game.Screens.Ranking
|
||||
private BindableList<UserTag> displayedTags { get; } = new BindableList<UserTag>();
|
||||
|
||||
private Bindable<APITag[]?> apiTags = null!;
|
||||
private BindableDictionary<long, UserTag> allTagsById { get; } = new BindableDictionary<long, UserTag>();
|
||||
private BindableDictionary<long, UserTag> relevantTagsById { get; } = new BindableDictionary<long, UserTag>();
|
||||
|
||||
private readonly Bindable<APIBeatmap?> apiBeatmap = new Bindable<APIBeatmap?>();
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace osu.Game.Screens.Ranking
|
||||
},
|
||||
new TagList
|
||||
{
|
||||
AvailableTags = { BindTarget = allTagsById },
|
||||
AvailableTags = { BindTarget = relevantTagsById },
|
||||
OnSelected = toggleVote,
|
||||
}
|
||||
}
|
||||
@@ -149,12 +149,14 @@ namespace osu.Game.Screens.Ranking
|
||||
if (apiTags.Value == null || apiBeatmap.Value == null)
|
||||
return;
|
||||
|
||||
allTagsById.Clear();
|
||||
allTagsById.AddRange(apiTags.Value.Select(t => new KeyValuePair<long, UserTag>(t.Id, new UserTag(t))));
|
||||
relevantTagsById.Clear();
|
||||
relevantTagsById.AddRange(apiTags.Value
|
||||
.Where(t => t.RulesetId == null || t.RulesetId == beatmapInfo.Ruleset.OnlineID)
|
||||
.Select(t => new KeyValuePair<long, UserTag>(t.Id, new UserTag(t))));
|
||||
|
||||
foreach (var topTag in apiBeatmap.Value.TopTags ?? [])
|
||||
{
|
||||
if (allTagsById.TryGetValue(topTag.TagId, out var tag))
|
||||
if (relevantTagsById.TryGetValue(topTag.TagId, out var tag))
|
||||
{
|
||||
tag.VoteCount.Value = topTag.VoteCount;
|
||||
displayedTags.Add(tag);
|
||||
@@ -163,7 +165,7 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
foreach (long ownTagId in apiBeatmap.Value.OwnTagIds ?? [])
|
||||
{
|
||||
if (allTagsById.TryGetValue(ownTagId, out var tag))
|
||||
if (relevantTagsById.TryGetValue(ownTagId, out var tag))
|
||||
tag.Voted.Value = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
|
||||
/// <param name="host">The room host.</param>
|
||||
public void AddServerSideRoom(Room room, APIUser host)
|
||||
{
|
||||
room.RoomID ??= currentRoomId++;
|
||||
room.RoomID = currentRoomId++;
|
||||
room.Host = host;
|
||||
|
||||
for (int i = 0; i < room.Playlist.Count; i++)
|
||||
|
||||
Reference in New Issue
Block a user