2020-12-20 23:04:06 +08:00
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
|
2020-12-29 15:20:43 +08:00
|
|
|
using System;
|
2021-02-01 16:54:56 +08:00
|
|
|
using System.Collections.Generic;
|
2020-12-20 23:04:06 +08:00
|
|
|
using System.Collections.Specialized;
|
2020-12-24 09:38:53 +08:00
|
|
|
using System.Diagnostics;
|
2020-12-20 23:04:06 +08:00
|
|
|
using System.Linq;
|
2020-12-29 15:20:43 +08:00
|
|
|
using JetBrains.Annotations;
|
2020-12-20 23:04:06 +08:00
|
|
|
using osu.Framework.Allocation;
|
2020-12-23 14:58:50 +08:00
|
|
|
using osu.Framework.Bindables;
|
2020-12-20 23:04:06 +08:00
|
|
|
using osu.Framework.Graphics;
|
|
|
|
using osu.Framework.Graphics.Containers;
|
|
|
|
using osu.Framework.Screens;
|
2021-02-10 18:56:59 +08:00
|
|
|
using osu.Framework.Threading;
|
|
|
|
using osu.Game.Configuration;
|
2020-12-20 23:04:06 +08:00
|
|
|
using osu.Game.Online.Multiplayer;
|
2020-12-25 12:38:11 +08:00
|
|
|
using osu.Game.Online.Rooms;
|
2021-02-01 12:40:59 +08:00
|
|
|
using osu.Game.Overlays.Mods;
|
2021-02-01 16:54:56 +08:00
|
|
|
using osu.Game.Rulesets.Mods;
|
2020-12-25 23:50:00 +08:00
|
|
|
using osu.Game.Screens.OnlinePlay.Components;
|
|
|
|
using osu.Game.Screens.OnlinePlay.Match;
|
|
|
|
using osu.Game.Screens.OnlinePlay.Match.Components;
|
|
|
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
|
|
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
|
2021-02-01 12:40:59 +08:00
|
|
|
using osu.Game.Screens.Play.HUD;
|
2020-12-20 23:04:06 +08:00
|
|
|
using osu.Game.Users;
|
2021-02-01 12:40:59 +08:00
|
|
|
using osuTK;
|
2020-12-25 23:50:00 +08:00
|
|
|
using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList;
|
2020-12-20 23:04:06 +08:00
|
|
|
|
2020-12-25 23:50:00 +08:00
|
|
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
|
|
|
[Cached]
|
2020-12-25 12:38:11 +08:00
|
|
|
public class MultiplayerMatchSubScreen : RoomSubScreen
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
|
|
|
public override string Title { get; }
|
|
|
|
|
2020-12-24 23:10:29 +08:00
|
|
|
public override string ShortTitle => "room";
|
2020-12-20 23:04:06 +08:00
|
|
|
|
|
|
|
[Resolved]
|
|
|
|
private StatefulMultiplayerClient client { get; set; }
|
|
|
|
|
2020-12-29 15:20:43 +08:00
|
|
|
[Resolved]
|
|
|
|
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
2020-12-29 03:59:38 +08:00
|
|
|
|
2021-02-01 16:57:32 +08:00
|
|
|
private ModSelectOverlay userModsSelectOverlay;
|
2020-12-25 12:38:11 +08:00
|
|
|
private MultiplayerMatchSettingsOverlay settingsOverlay;
|
2021-02-01 16:57:32 +08:00
|
|
|
private Drawable userModsSection;
|
2020-12-20 23:04:06 +08:00
|
|
|
|
2020-12-23 14:58:50 +08:00
|
|
|
private IBindable<bool> isConnected;
|
|
|
|
|
2020-12-29 15:20:43 +08:00
|
|
|
[CanBeNull]
|
2020-12-31 01:00:57 +08:00
|
|
|
private IDisposable readyClickOperation;
|
2020-12-29 15:20:43 +08:00
|
|
|
|
2021-01-19 21:52:43 +08:00
|
|
|
private GridContainer mainContent;
|
|
|
|
|
2020-12-25 12:38:11 +08:00
|
|
|
public MultiplayerMatchSubScreen(Room room)
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
2020-12-24 23:10:29 +08:00
|
|
|
Title = room.RoomID.Value == null ? "New room" : room.Name.Value;
|
2020-12-20 23:04:06 +08:00
|
|
|
Activity.Value = new UserActivity.InLobby(room);
|
|
|
|
}
|
|
|
|
|
|
|
|
[BackgroundDependencyLoader]
|
|
|
|
private void load()
|
|
|
|
{
|
2021-01-17 04:02:30 +08:00
|
|
|
AddRangeInternal(new Drawable[]
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
2021-01-19 21:52:43 +08:00
|
|
|
mainContent = new GridContainer
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
Content = new[]
|
|
|
|
{
|
|
|
|
new Drawable[]
|
|
|
|
{
|
|
|
|
new Container
|
|
|
|
{
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
Padding = new MarginPadding
|
|
|
|
{
|
|
|
|
Horizontal = 105,
|
|
|
|
Vertical = 20
|
|
|
|
},
|
|
|
|
Child = new GridContainer
|
|
|
|
{
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
RowDimensions = new[]
|
|
|
|
{
|
|
|
|
new Dimension(GridSizeMode.AutoSize),
|
|
|
|
new Dimension(),
|
|
|
|
},
|
|
|
|
Content = new[]
|
|
|
|
{
|
|
|
|
new Drawable[]
|
|
|
|
{
|
2020-12-25 12:38:11 +08:00
|
|
|
new MultiplayerMatchHeader
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
|
|
|
OpenSettings = () => settingsOverlay.Show()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
new Drawable[]
|
|
|
|
{
|
2021-02-03 13:52:36 +08:00
|
|
|
new Container
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
2021-02-03 13:52:36 +08:00
|
|
|
Padding = new MarginPadding { Horizontal = 5, Vertical = 10 },
|
|
|
|
Child = new GridContainer
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
2021-02-03 13:52:36 +08:00
|
|
|
RelativeSizeAxes = Axes.Both,
|
2021-02-05 12:05:11 +08:00
|
|
|
ColumnDimensions = new[]
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
2021-02-05 12:05:11 +08:00
|
|
|
new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400),
|
|
|
|
new Dimension(),
|
|
|
|
new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600),
|
|
|
|
},
|
2021-02-03 13:52:36 +08:00
|
|
|
Content = new[]
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
2021-02-03 13:52:36 +08:00
|
|
|
new Drawable[]
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
2021-02-03 13:52:36 +08:00
|
|
|
// Main left column
|
|
|
|
new GridContainer
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
RowDimensions = new[]
|
|
|
|
{
|
|
|
|
new Dimension(GridSizeMode.AutoSize)
|
|
|
|
},
|
|
|
|
Content = new[]
|
|
|
|
{
|
|
|
|
new Drawable[] { new ParticipantsListHeader() },
|
|
|
|
new Drawable[]
|
|
|
|
{
|
2020-12-23 15:19:03 +08:00
|
|
|
new ParticipantsList
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
|
|
|
RelativeSizeAxes = Axes.Both
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2021-02-03 13:52:36 +08:00
|
|
|
},
|
2021-02-05 12:05:11 +08:00
|
|
|
// Spacer
|
|
|
|
null,
|
2021-02-03 13:52:36 +08:00
|
|
|
// Main right column
|
|
|
|
new FillFlowContainer
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
2021-02-03 13:52:36 +08:00
|
|
|
RelativeSizeAxes = Axes.X,
|
|
|
|
AutoSizeAxes = Axes.Y,
|
|
|
|
Children = new[]
|
2021-02-01 12:40:59 +08:00
|
|
|
{
|
2021-02-03 13:52:36 +08:00
|
|
|
new FillFlowContainer
|
2021-02-01 12:40:59 +08:00
|
|
|
{
|
2021-02-03 13:52:36 +08:00
|
|
|
RelativeSizeAxes = Axes.X,
|
|
|
|
AutoSizeAxes = Axes.Y,
|
|
|
|
Children = new Drawable[]
|
2021-02-01 12:40:59 +08:00
|
|
|
{
|
2021-02-03 13:52:36 +08:00
|
|
|
new OverlinedHeader("Beatmap"),
|
|
|
|
new BeatmapSelectionControl { RelativeSizeAxes = Axes.X }
|
|
|
|
}
|
|
|
|
},
|
|
|
|
userModsSection = new FillFlowContainer
|
|
|
|
{
|
|
|
|
RelativeSizeAxes = Axes.X,
|
|
|
|
AutoSizeAxes = Axes.Y,
|
|
|
|
Margin = new MarginPadding { Top = 10 },
|
|
|
|
Children = new Drawable[]
|
2021-02-01 12:40:59 +08:00
|
|
|
{
|
2021-02-03 13:52:36 +08:00
|
|
|
new OverlinedHeader("Extra mods"),
|
|
|
|
new FillFlowContainer
|
|
|
|
{
|
|
|
|
AutoSizeAxes = Axes.Both,
|
|
|
|
Direction = FillDirection.Horizontal,
|
|
|
|
Spacing = new Vector2(10, 0),
|
|
|
|
Children = new Drawable[]
|
|
|
|
{
|
|
|
|
new PurpleTriangleButton
|
|
|
|
{
|
|
|
|
Anchor = Anchor.CentreLeft,
|
|
|
|
Origin = Anchor.CentreLeft,
|
|
|
|
Width = 90,
|
|
|
|
Text = "Select",
|
|
|
|
Action = () => userModsSelectOverlay.Show()
|
|
|
|
},
|
|
|
|
new ModDisplay
|
|
|
|
{
|
|
|
|
Anchor = Anchor.CentreLeft,
|
|
|
|
Origin = Anchor.CentreLeft,
|
|
|
|
DisplayUnrankedText = false,
|
|
|
|
Current = UserMods,
|
|
|
|
Scale = new Vector2(0.8f),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
2021-02-01 12:40:59 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-12-20 23:04:06 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
new Drawable[]
|
|
|
|
{
|
|
|
|
new GridContainer
|
|
|
|
{
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
RowDimensions = new[]
|
|
|
|
{
|
|
|
|
new Dimension(GridSizeMode.AutoSize)
|
|
|
|
},
|
|
|
|
Content = new[]
|
|
|
|
{
|
|
|
|
new Drawable[] { new OverlinedHeader("Chat") },
|
|
|
|
new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
new Drawable[]
|
|
|
|
{
|
2020-12-29 14:51:46 +08:00
|
|
|
new MultiplayerMatchFooter
|
|
|
|
{
|
2020-12-31 01:00:57 +08:00
|
|
|
OnReadyClick = onReadyClick
|
2020-12-29 14:51:46 +08:00
|
|
|
}
|
2020-12-20 23:04:06 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
RowDimensions = new[]
|
|
|
|
{
|
|
|
|
new Dimension(),
|
|
|
|
new Dimension(GridSizeMode.AutoSize),
|
|
|
|
}
|
|
|
|
},
|
2021-02-01 17:18:59 +08:00
|
|
|
new Container
|
2021-02-01 12:40:59 +08:00
|
|
|
{
|
2021-02-01 17:18:59 +08:00
|
|
|
Anchor = Anchor.BottomLeft,
|
|
|
|
Origin = Anchor.BottomLeft,
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
Height = 0.5f,
|
2021-02-01 17:54:47 +08:00
|
|
|
Child = userModsSelectOverlay = new UserModSelectOverlay
|
2021-02-01 17:18:59 +08:00
|
|
|
{
|
|
|
|
SelectedMods = { BindTarget = UserMods },
|
|
|
|
IsValidMod = _ => false
|
|
|
|
}
|
2021-02-01 12:40:59 +08:00
|
|
|
},
|
2020-12-25 12:38:11 +08:00
|
|
|
settingsOverlay = new MultiplayerMatchSettingsOverlay
|
2020-12-20 23:04:06 +08:00
|
|
|
{
|
|
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden }
|
|
|
|
}
|
2021-01-17 04:02:30 +08:00
|
|
|
});
|
2021-01-19 05:08:06 +08:00
|
|
|
|
2021-01-19 21:52:43 +08:00
|
|
|
if (client.Room == null)
|
|
|
|
{
|
2021-01-19 22:16:39 +08:00
|
|
|
// A new room is being created.
|
|
|
|
// The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed.
|
2021-01-19 21:52:43 +08:00
|
|
|
mainContent.Hide();
|
|
|
|
|
|
|
|
settingsOverlay.State.BindValueChanged(visibility =>
|
|
|
|
{
|
|
|
|
if (visibility.NewValue == Visibility.Hidden)
|
|
|
|
mainContent.Show();
|
|
|
|
}, true);
|
|
|
|
}
|
2020-12-20 23:04:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
protected override void LoadComplete()
|
|
|
|
{
|
|
|
|
base.LoadComplete();
|
|
|
|
|
|
|
|
Playlist.BindCollectionChanged(onPlaylistChanged, true);
|
2021-01-18 22:23:51 +08:00
|
|
|
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
|
2021-02-01 16:57:32 +08:00
|
|
|
UserMods.BindValueChanged(onUserModsChanged);
|
2020-12-20 23:04:06 +08:00
|
|
|
|
|
|
|
client.LoadRequested += onLoadRequested;
|
2020-12-23 14:58:50 +08:00
|
|
|
|
|
|
|
isConnected = client.IsConnected.GetBoundCopy();
|
|
|
|
isConnected.BindValueChanged(connected =>
|
|
|
|
{
|
|
|
|
if (!connected.NewValue)
|
|
|
|
Schedule(this.Exit);
|
|
|
|
}, true);
|
2020-12-20 23:04:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
public override bool OnBackButton()
|
|
|
|
{
|
|
|
|
if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible)
|
|
|
|
{
|
|
|
|
settingsOverlay.Hide();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-02-01 16:57:32 +08:00
|
|
|
if (userModsSelectOverlay.State.Value == Visibility.Visible)
|
2021-02-01 12:40:59 +08:00
|
|
|
{
|
2021-02-01 16:57:32 +08:00
|
|
|
userModsSelectOverlay.Hide();
|
2021-02-01 12:40:59 +08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-12-20 23:04:06 +08:00
|
|
|
return base.OnBackButton();
|
|
|
|
}
|
|
|
|
|
2021-02-01 12:40:59 +08:00
|
|
|
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
|
|
{
|
|
|
|
SelectedItem.Value = Playlist.FirstOrDefault();
|
|
|
|
|
|
|
|
if (SelectedItem.Value?.AllowedMods.Any() != true)
|
|
|
|
{
|
2021-02-01 16:57:32 +08:00
|
|
|
userModsSection.Hide();
|
|
|
|
userModsSelectOverlay.Hide();
|
|
|
|
userModsSelectOverlay.IsValidMod = _ => false;
|
2021-02-01 12:40:59 +08:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-02-01 16:57:32 +08:00
|
|
|
userModsSection.Show();
|
|
|
|
userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType());
|
2021-02-01 12:40:59 +08:00
|
|
|
}
|
|
|
|
}
|
2020-12-20 23:04:06 +08:00
|
|
|
|
2021-02-10 18:56:59 +08:00
|
|
|
private ModSettingChangeTracker modSettingChangeTracker;
|
|
|
|
private ScheduledDelegate debouncedModSettingsUpdate;
|
|
|
|
|
2021-02-01 16:57:32 +08:00
|
|
|
private void onUserModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
2021-02-01 16:54:56 +08:00
|
|
|
{
|
2021-02-10 18:56:59 +08:00
|
|
|
modSettingChangeTracker?.Dispose();
|
|
|
|
|
2021-02-01 16:54:56 +08:00
|
|
|
if (client.Room == null)
|
|
|
|
return;
|
|
|
|
|
2021-02-01 18:28:33 +08:00
|
|
|
client.ChangeUserMods(mods.NewValue);
|
2021-02-10 18:56:59 +08:00
|
|
|
|
|
|
|
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
|
|
|
|
modSettingChangeTracker.SettingChanged += onModSettingsChanged;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void onModSettingsChanged(Mod mod)
|
|
|
|
{
|
|
|
|
// Debounce changes to mod settings so as to not thrash the network.
|
|
|
|
debouncedModSettingsUpdate?.Cancel();
|
2021-02-10 19:16:26 +08:00
|
|
|
debouncedModSettingsUpdate = Scheduler.AddDelayed(() =>
|
|
|
|
{
|
|
|
|
if (client.Room == null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
client.ChangeUserMods(UserMods.Value);
|
|
|
|
}, 500);
|
2021-02-01 16:54:56 +08:00
|
|
|
}
|
2020-12-20 23:04:06 +08:00
|
|
|
|
2021-02-05 15:17:02 +08:00
|
|
|
private void updateBeatmapAvailability(ValueChangedEvent<BeatmapAvailability> availability)
|
2021-01-18 15:49:38 +08:00
|
|
|
{
|
2021-01-18 22:23:51 +08:00
|
|
|
if (client.Room == null)
|
|
|
|
return;
|
|
|
|
|
2021-02-05 15:17:02 +08:00
|
|
|
client.ChangeBeatmapAvailability(availability.NewValue);
|
2021-02-05 15:19:45 +08:00
|
|
|
|
|
|
|
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
|
2021-02-05 16:17:29 +08:00
|
|
|
if (availability.NewValue != Online.Rooms.BeatmapAvailability.LocallyAvailable()
|
|
|
|
&& client.LocalUser?.State == MultiplayerUserState.Ready)
|
2021-02-05 15:19:45 +08:00
|
|
|
client.ChangeState(MultiplayerUserState.Idle);
|
2021-01-18 15:49:38 +08:00
|
|
|
}
|
|
|
|
|
2020-12-31 01:00:57 +08:00
|
|
|
private void onReadyClick()
|
2020-12-29 14:51:46 +08:00
|
|
|
{
|
2020-12-31 01:00:57 +08:00
|
|
|
Debug.Assert(readyClickOperation == null);
|
|
|
|
readyClickOperation = ongoingOperationTracker.BeginOperation();
|
|
|
|
|
|
|
|
if (client.IsHost && client.LocalUser?.State == MultiplayerUserState.Ready)
|
|
|
|
{
|
|
|
|
client.StartMatch()
|
|
|
|
.ContinueWith(t =>
|
|
|
|
{
|
|
|
|
// accessing Exception here silences any potential errors from the antecedent task
|
|
|
|
if (t.Exception != null)
|
|
|
|
{
|
|
|
|
// gameplay was not started due to an exception; unblock button.
|
|
|
|
endOperation();
|
|
|
|
}
|
|
|
|
|
|
|
|
// gameplay is starting, the button will be unblocked on load requested.
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2020-12-29 16:09:47 +08:00
|
|
|
|
|
|
|
client.ToggleReady()
|
2021-01-29 15:32:28 +08:00
|
|
|
.ContinueWith(t => endOperation());
|
2020-12-31 01:00:57 +08:00
|
|
|
|
|
|
|
void endOperation()
|
|
|
|
{
|
2021-01-13 07:58:53 +08:00
|
|
|
readyClickOperation?.Dispose();
|
2020-12-31 01:00:57 +08:00
|
|
|
readyClickOperation = null;
|
|
|
|
}
|
2020-12-29 14:51:46 +08:00
|
|
|
}
|
|
|
|
|
2020-12-24 09:38:53 +08:00
|
|
|
private void onLoadRequested()
|
|
|
|
{
|
|
|
|
Debug.Assert(client.Room != null);
|
|
|
|
|
2020-12-29 13:27:33 +08:00
|
|
|
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
|
2020-12-24 09:38:53 +08:00
|
|
|
|
2020-12-25 12:38:11 +08:00
|
|
|
StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds));
|
2020-12-29 15:20:43 +08:00
|
|
|
|
2021-01-13 07:58:53 +08:00
|
|
|
readyClickOperation?.Dispose();
|
2020-12-31 01:00:57 +08:00
|
|
|
readyClickOperation = null;
|
2020-12-24 09:38:53 +08:00
|
|
|
}
|
2020-12-20 23:04:06 +08:00
|
|
|
|
|
|
|
protected override void Dispose(bool isDisposing)
|
|
|
|
{
|
|
|
|
base.Dispose(isDisposing);
|
|
|
|
|
|
|
|
if (client != null)
|
|
|
|
client.LoadRequested -= onLoadRequested;
|
2021-02-10 19:09:45 +08:00
|
|
|
|
|
|
|
modSettingChangeTracker?.Dispose();
|
2020-12-20 23:04:06 +08:00
|
|
|
}
|
2021-02-01 17:54:47 +08:00
|
|
|
|
2021-02-05 15:42:35 +08:00
|
|
|
private class UserModSelectOverlay : LocalPlayerModSelectOverlay
|
2021-02-01 17:54:47 +08:00
|
|
|
{
|
|
|
|
}
|
2020-12-20 23:04:06 +08:00
|
|
|
}
|
|
|
|
}
|