Fixed inability to connect via port

Added Scrollbar to choose Relic with more than 4 players
Added custom player name feature, Now host will synchronize client names
This commit is contained in:
kmyuhkyuk
2026-03-15 08:06:26 +08:00
parent 24a06e3a00
commit 6ff7a9dc2b
25 changed files with 785 additions and 107 deletions
@@ -8,7 +8,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Components
{ {
public override void _Ready() public override void _Ready()
{ {
TextSubmitted += OnTextSubmitted; TextChanged += OnTextChanged;
} }
public struct AddressInfo(bool isValid, string? address, ushort? port) public struct AddressInfo(bool isValid, string? address, ushort? port)
@@ -50,13 +50,14 @@ namespace SlayTheSpire2.LAN.Multiplayer.Components
else if (IPEndPoint.TryParse(Text, out var ipEndPoint)) else if (IPEndPoint.TryParse(Text, out var ipEndPoint))
{ {
addressInfo.IsValid = true; addressInfo.IsValid = true;
addressInfo.Address = ipEndPoint.Address.ToString();
addressInfo.Port = (ushort)ipEndPoint.Port; addressInfo.Port = (ushort)ipEndPoint.Port;
} }
return addressInfo; return addressInfo;
} }
private void OnTextSubmitted(string newText) private void OnTextChanged(string newText)
{ {
Modulate = Colors.White; Modulate = Colors.White;
@@ -0,0 +1,37 @@
using System.Text.RegularExpressions;
using Godot;
using MegaCrit.Sts2.Core.Nodes.GodotExtensions;
namespace SlayTheSpire2.LAN.Multiplayer.Components
{
internal partial class PlayerNameLineEdit : NMegaLineEdit
{
[GeneratedRegex(@"[\x00-\x1F\x7F<>:""/\\|?*\x0B\x0C\x0D&;`#$%^+={}]")]
private static partial Regex InvalidCharsRegex();
public bool IsInvalid => !GetPlayerNameIsInvalid(Text);
public bool IsEmpty => string.IsNullOrEmpty(Text);
public PlayerNameLineEdit()
{
TextChanged += OnTextChanged;
}
private void OnTextChanged(string newText)
{
Modulate = Colors.White;
if (string.IsNullOrEmpty(newText) || !GetPlayerNameIsInvalid(newText))
return;
Modulate = Colors.Red;
}
public static bool GetPlayerNameIsInvalid(string playerName)
{
return string.IsNullOrWhiteSpace(playerName) || playerName.StartsWith(' ') ||
InvalidCharsRegex().IsMatch(playerName);
}
}
}
@@ -0,0 +1,119 @@
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.addons.mega_text;
using MegaCrit.Sts2.Core.Multiplayer;
using MegaCrit.Sts2.Core.Multiplayer.Game;
using MegaCrit.Sts2.Core.Nodes.Multiplayer;
using MegaCrit.Sts2.Core.Nodes.Screens.CharacterSelect;
using MegaCrit.Sts2.Core.Platform;
using SlayTheSpire2.LAN.Multiplayer.Components;
using SlayTheSpire2.LAN.Multiplayer.Models;
// ReSharper disable ClassNeverInstantiated.Global
namespace SlayTheSpire2.LAN.Multiplayer.Helpers
{
internal class LanPlayerNameHelper
{
public static LanPlayerNames PlayerNameDictionary = DefaultPlayerNameDictionary;
public static INetGameService? NetService;
public static NCharacterSelectScreen? CharacterSelectScreen;
public static NMultiplayerLoadGameScreen? MultiplayerLoadGameScreen;
public static TaskCompletionSource<LanPlayerNameResponseMessage>? LanPlayerNameCompletion;
private static LanPlayerNames DefaultPlayerNameDictionary =>
!PlayerNameLineEdit.GetPlayerNameIsInvalid(SettingsHelper.Instance.SettingsModel.PlayerName)
? new LanPlayerNames
{
{ 1u, SettingsHelper.Instance.SettingsModel.PlayerName }
}
: new LanPlayerNames();
public static void SetHostPlayerName()
{
if (!PlayerNameLineEdit.GetPlayerNameIsInvalid(SettingsHelper.Instance.SettingsModel.PlayerName))
{
PlayerNameDictionary[1u] = SettingsHelper.Instance.SettingsModel.PlayerName;
}
else
{
PlayerNameDictionary.Remove(1u);
}
}
public static void SetDefaultPlayerNameDictionary()
{
PlayerNameDictionary = DefaultPlayerNameDictionary;
}
public static void HandleLanPlayerNameResponseMessage(LanPlayerNameResponseMessage lanPlayerNameResponseMessage,
ulong senderId)
{
PlayerNameDictionary = lanPlayerNameResponseMessage.playerNameDictionary;
UpdatePlayerName();
LanPlayerNameCompletion?.SetResult(lanPlayerNameResponseMessage);
}
public static void HandleLanPlayerNameRequestMessage(LanPlayerNameRequestMessage lanPlayerNameRequestMessage,
ulong senderId)
{
if (PlayerNameLineEdit.GetPlayerNameIsInvalid(lanPlayerNameRequestMessage.playerName))
{
NetService?.SendMessage(
new LanPlayerNameResponseMessage { playerNameDictionary = PlayerNameDictionary }, senderId);
return;
}
PlayerNameDictionary[senderId] = lanPlayerNameRequestMessage.playerName;
UpdatePlayerName();
NetService?.SendMessage(new LanPlayerNameResponseMessage { playerNameDictionary = PlayerNameDictionary });
}
public static async Task AttemptPlayerName(NetClientGameService gameService)
{
LanPlayerNameCompletion = new TaskCompletionSource<LanPlayerNameResponseMessage>();
var message = new LanPlayerNameRequestMessage
{ playerName = SettingsHelper.Instance.SettingsModel.PlayerName };
gameService.SendMessage(message);
await LanPlayerNameCompletion.Task;
LanPlayerNameCompletion = null;
}
private static void UpdatePlayerName()
{
if (CharacterSelectScreen != null)
{
UpdateNameplateLabel(CharacterSelectScreen);
}
if (MultiplayerLoadGameScreen != null)
{
UpdateNameplateLabel(MultiplayerLoadGameScreen);
}
}
private static void UpdateNameplateLabel(Control container)
{
var nodes = Traverse.Create(container).Field("_remotePlayerContainer")
.Field("_nodes").GetValue<List<NRemoteLobbyPlayer>>();
foreach (var node in nodes)
{
var megaLabel = Traverse.Create(node).Field("_nameplateLabel").GetValue<MegaLabel>();
if (!GodotObject.IsInstanceValid(megaLabel))
continue;
megaLabel.SetTextAutoSize(PlatformUtil.GetPlayerName(PlatformType.None, node.PlayerId));
}
}
}
}
@@ -1,9 +1,11 @@
using HarmonyLib; using System.Text.Json;
using HarmonyLib;
using MegaCrit.Sts2.Core.Logging; using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Runs; using MegaCrit.Sts2.Core.Runs;
using MegaCrit.Sts2.Core.Saves; using MegaCrit.Sts2.Core.Saves;
using MegaCrit.Sts2.Core.Saves.Managers; using MegaCrit.Sts2.Core.Saves.Managers;
using MegaCrit.Sts2.Core.Saves.Migrations; using MegaCrit.Sts2.Core.Saves.Migrations;
using SlayTheSpire2.LAN.Multiplayer.Models;
// ReSharper disable MemberCanBePrivate.Global // ReSharper disable MemberCanBePrivate.Global
// ReSharper disable ClassNeverInstantiated.Global // ReSharper disable ClassNeverInstantiated.Global
@@ -23,6 +25,9 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
public static string CurrentMultiplayerRunSavePath => public static string CurrentMultiplayerRunSavePath =>
RunSaveManager.GetRunSavePath(ProfileIdProvider.CurrentProfileId, "current_lan_run_mp.save"); RunSaveManager.GetRunSavePath(ProfileIdProvider.CurrentProfileId, "current_lan_run_mp.save");
public static string CurrentMultiplayerRunPlayerNamesPath =>
RunSaveManager.GetRunSavePath(ProfileIdProvider.CurrentProfileId, "current_lan_run_mp_player_names.json");
public static bool HasMultiplayerRunSave => SaveStore.FileExists(CurrentMultiplayerRunSavePath); public static bool HasMultiplayerRunSave => SaveStore.FileExists(CurrentMultiplayerRunSavePath);
public static ReadSaveResult<SerializableRun> LoadAndCanonicalizeMultiplayerRunSave(ulong localPlayerId) public static ReadSaveResult<SerializableRun> LoadAndCanonicalizeMultiplayerRunSave(ulong localPlayerId)
@@ -33,6 +38,14 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
try try
{ {
var data = RunManager.CanonicalizeSave(readSaveResult.SaveData, localPlayerId); var data = RunManager.CanonicalizeSave(readSaveResult.SaveData, localPlayerId);
var playerNamesJson = SaveStore.ReadFile(CurrentMultiplayerRunPlayerNamesPath);
if (!string.IsNullOrEmpty(playerNamesJson))
{
LanPlayerNameHelper.PlayerNameDictionary =
JsonSerializer.Deserialize<LanPlayerNames>(playerNamesJson) ?? new LanPlayerNames();
}
LanPlayerNameHelper.SetHostPlayerName();
return new ReadSaveResult<SerializableRun>(data, ReadSaveStatus.Success); return new ReadSaveResult<SerializableRun>(data, ReadSaveStatus.Success);
} }
catch (Exception value) catch (Exception value)
@@ -76,6 +89,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
public static void DeleteCurrentMultiplayerRun() public static void DeleteCurrentMultiplayerRun()
{ {
SaveStore.DeleteFile(CurrentMultiplayerRunSavePath); SaveStore.DeleteFile(CurrentMultiplayerRunSavePath);
SaveStore.DeleteFile(CurrentMultiplayerRunPlayerNamesPath);
} }
public static void RenameBrokenMultiplayerRunSave(ReadSaveStatus status) public static void RenameBrokenMultiplayerRunSave(ReadSaveStatus status)
@@ -0,0 +1,25 @@
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Multiplayer.Serialization;
using MegaCrit.Sts2.Core.Multiplayer.Transport;
namespace SlayTheSpire2.LAN.Multiplayer.Models
{
public struct LanPlayerNameRequestMessage : INetMessage
{
public string playerName;
public bool ShouldBroadcast => false;
public NetTransferMode Mode => NetTransferMode.Reliable;
public LogLevel LogLevel => LogLevel.Info;
public void Serialize(PacketWriter writer)
{
writer.WriteString(playerName);
}
public void Deserialize(PacketReader reader)
{
playerName = reader.ReadString();
}
}
}
@@ -0,0 +1,35 @@
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Multiplayer.Serialization;
using MegaCrit.Sts2.Core.Multiplayer.Transport;
namespace SlayTheSpire2.LAN.Multiplayer.Models
{
public struct LanPlayerNameResponseMessage : INetMessage
{
public LanPlayerNames playerNameDictionary;
public bool ShouldBroadcast => false;
public NetTransferMode Mode => NetTransferMode.Reliable;
public LogLevel LogLevel => LogLevel.Info;
public void Serialize(PacketWriter writer)
{
writer.WriteInt(playerNameDictionary.Count);
foreach (var keyValue in playerNameDictionary)
{
writer.WriteULong(keyValue.Key);
writer.WriteString(keyValue.Value);
}
}
public void Deserialize(PacketReader reader)
{
var count = reader.ReadInt();
playerNameDictionary = new LanPlayerNames();
for (var i = 0; i < count; i++)
{
playerNameDictionary.Add(reader.ReadULong(), reader.ReadString());
}
}
}
}
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace SlayTheSpire2.LAN.Multiplayer.Models
{
[JsonSerializable(typeof(LanPlayerNames))]
public partial class LanPlayerNamesContext : JsonSerializerContext;
public class LanPlayerNames : Dictionary<ulong, string>;
}
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using MegaCrit.Sts2.Core.Saves; using MegaCrit.Sts2.Core.Saves;
using Steamworks;
namespace SlayTheSpire2.LAN.Multiplayer.Models namespace SlayTheSpire2.LAN.Multiplayer.Models
{ {
@@ -12,6 +13,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Models
[JsonPropertyName("host_max_players")] public int HostMaxPlayers { get; set; } = 4; [JsonPropertyName("host_max_players")] public int HostMaxPlayers { get; set; } = 4;
[JsonPropertyName("ip_address")] public string IPAddress { get; set; } = "127.0.0.1"; [JsonPropertyName("ip_address")] public string IPAddress { get; set; } = "127.0.0.1";
[JsonPropertyName("net_id")] public ulong NetId { get; set; } = 1000u; [JsonPropertyName("net_id")] public ulong NetId { get; set; } = 1000u;
[JsonPropertyName("player_name")] public string PlayerName { get; set; } = SteamFriends.GetPersonaName();
public int SchemaVersion { get; set; } public int SchemaVersion { get; set; }
} }
@@ -10,7 +10,7 @@ using Logger = MegaCrit.Sts2.Core.Logging.Logger;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs namespace SlayTheSpire2.LAN.Multiplayer.Patchs.ENet
{ {
[HarmonyPatch(typeof(ENetClient), "ConnectToHost")] [HarmonyPatch(typeof(ENetClient), "ConnectToHost")]
internal class ENetClientConnectToHostPatch internal class ENetClientConnectToHostPatch
@@ -132,7 +132,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
if (eNetHandshakeResponse.status == ENetHandshakeStatus.IdCollision) if (eNetHandshakeResponse.status == ENetHandshakeStatus.IdCollision)
{ {
logger.Warn( logger.Warn(
$"NetID:{netId} already occupied, Next try server send new NetID:{eNetHandshakeResponse.newNetId}"); $"NetID:{netId} already occupied, Next try host send new NetID:{eNetHandshakeResponse.newNetId}");
return (new NetErrorInfo(NetError.Kicked, selfInitiated: false), return (new NetErrorInfo(NetError.Kicked, selfInitiated: false),
eNetHandshakeResponse.newNetId); eNetHandshakeResponse.newNetId);
} }
@@ -12,7 +12,7 @@ using Logger = MegaCrit.Sts2.Core.Logging.Logger;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs namespace SlayTheSpire2.LAN.Multiplayer.Patchs.ENet
{ {
[HarmonyPatch(typeof(ENetHost), "DoClientHandshake")] [HarmonyPatch(typeof(ENetHost), "DoClientHandshake")]
internal class ENetHostDoClientHandshakePatch internal class ENetHostDoClientHandshakePatch
@@ -0,0 +1,120 @@
using HarmonyLib;
using MegaCrit.Sts2.Core.Entities.Multiplayer;
using MegaCrit.Sts2.Core.Multiplayer.Connection;
using MegaCrit.Sts2.Core.Multiplayer.Game;
using MegaCrit.Sts2.Core.Multiplayer.Messages.Lobby;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
using SlayTheSpire2.LAN.Multiplayer.Models;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
[HarmonyPatch(typeof(JoinFlow), "AttemptJoin")]
internal class JoinFlowAttemptJoinPatch
{
private static void Postfix(JoinFlow __instance, ref Task<ClientLobbyJoinResponseMessage> __result)
{
__result = AttemptJoin(__instance, __result);
}
private static async Task<ClientLobbyJoinResponseMessage> AttemptJoin(JoinFlow joinFlow,
Task<ClientLobbyJoinResponseMessage> clientLobbyJoinResponseMessage)
{
var result = await clientLobbyJoinResponseMessage;
if (joinFlow.NetService == null)
return result;
joinFlow.NetService.RegisterMessageHandler<LanPlayerNameResponseMessage>(LanPlayerNameHelper
.HandleLanPlayerNameResponseMessage);
await LanPlayerNameHelper.AttemptPlayerName(joinFlow.NetService);
return result;
}
}
[HarmonyPatch(typeof(JoinFlow), "AttemptLoadJoin")]
internal class JoinFlowAttemptLoadJoinPatch
{
private static void Postfix(JoinFlow __instance, ref Task<ClientLoadJoinResponseMessage> __result)
{
__result = AttemptLoadJoin(__instance, __result);
}
private static async Task<ClientLoadJoinResponseMessage> AttemptLoadJoin(JoinFlow joinFlow,
Task<ClientLoadJoinResponseMessage> clientLoadJoinResponseMessage)
{
var result = await clientLoadJoinResponseMessage;
if (joinFlow.NetService == null)
return result;
joinFlow.NetService.RegisterMessageHandler<LanPlayerNameResponseMessage>(LanPlayerNameHelper
.HandleLanPlayerNameResponseMessage);
await LanPlayerNameHelper.AttemptPlayerName(joinFlow.NetService);
return result;
}
}
[HarmonyPatch(typeof(JoinFlow), "AttemptRejoin")]
internal class JoinFlowAttemptRejoinPatch
{
private static void Postfix(JoinFlow __instance, ref Task<ClientRejoinResponseMessage> __result)
{
__result = AttemptRejoin(__instance, __result);
}
private static async Task<ClientRejoinResponseMessage> AttemptRejoin(JoinFlow joinFlow,
Task<ClientRejoinResponseMessage> clientRejoinResponseMessage)
{
var result = await clientRejoinResponseMessage;
if (joinFlow.NetService == null)
return result;
joinFlow.NetService.RegisterMessageHandler<LanPlayerNameResponseMessage>(LanPlayerNameHelper
.HandleLanPlayerNameResponseMessage);
await LanPlayerNameHelper.AttemptPlayerName(joinFlow.NetService);
return result;
}
}
[HarmonyPatch(typeof(JoinFlow), "OnDisconnected")]
internal class JoinFlowOnDisconnectedPatch
{
private static void Postfix(NetErrorInfo info)
{
var exception =
new ClientConnectionFailedException(
$"Unexpectedly disconnected from host while joining. Reason: {info.GetReason()}", info);
var lanPlayerNameCompletion = LanPlayerNameHelper.LanPlayerNameCompletion;
if (lanPlayerNameCompletion?.Task is { IsCompleted: false })
{
lanPlayerNameCompletion.SetException(exception);
}
}
}
[HarmonyPatch(typeof(JoinFlow), "Cancel")]
internal class JoinFlowCancelPatch
{
private static void Postfix()
{
var lanPlayerNameCompletion = LanPlayerNameHelper.LanPlayerNameCompletion;
if (lanPlayerNameCompletion?.Task is { IsCompleted: false })
{
lanPlayerNameCompletion.SetCanceled();
}
}
}
}
@@ -1,67 +0,0 @@
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.Core.Nodes.Screens.TreasureRoomRelic;
using MegaCrit.Sts2.Core.Runs;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
[HarmonyPatch(typeof(NTreasureRoomRelicCollection), "_Ready")]
internal class NTreasureRoomRelicCollectionPatch
{
private static void Prefix(NTreasureRoomRelicCollection __instance)
{
var runState = Traverse.Create(RunManager.Instance).Property("State").GetValue<RunState?>();
if (runState is { Players.Count: > 4 })
{
var container = __instance.GetNode("Container");
var multiplayerRelicHolder = container.GetNode("MultiplayerRelicHolder4");
for (var i = 4; i < runState.Players.Count; i++)
{
var newMultiplayerRelicHolder = multiplayerRelicHolder.Duplicate();
container.AddChild(newMultiplayerRelicHolder);
if (newMultiplayerRelicHolder is NTreasureRoomRelicHolder nTreasureRoomRelicHolder)
{
nTreasureRoomRelicHolder.Name = $"MultiplayerRelicHolder{i + 1}";
}
}
}
}
private static void Postfix(List<NTreasureRoomRelicHolder> ____multiplayerHolders)
{
if (____multiplayerHolders.Count > 4)
{
var more16People = ____multiplayerHolders.Count > 16;
var position = new Vector2(more16People ? 30 : 62, 110);
for (var i = 0; i < ____multiplayerHolders.Count; i++)
{
if (i > 0)
{
if (more16People)
{
position = i % 6 == 0
? new Vector2(30, position.Y + 140)
: new Vector2(position.X + 140, position.Y);
}
else
{
position = i % 4 == 0
? new Vector2(62, position.Y + 140)
: new Vector2(position.X + 240, position.Y);
}
}
____multiplayerHolders[i].Position = position;
}
}
}
}
}
@@ -0,0 +1,45 @@
using HarmonyLib;
using MegaCrit.Sts2.Core.Platform.Null;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
[HarmonyPatch(typeof(NullPlatformUtilStrategy), "GetPlayerName")]
internal class NullPlatformUtilStrategyGetPlayerNamePatch
{
private static bool Prefix(ulong playerId, List<NullMultiplayerName>? ____mpNames, ref string __result)
{
if (____mpNames != null)
{
foreach (var mpName in ____mpNames)
{
if (mpName.netId == playerId)
{
__result = mpName.name;
return false;
}
}
}
if (LanPlayerNameHelper.PlayerNameDictionary.TryGetValue(playerId, out var playerName))
{
__result = playerName;
return false;
}
__result = playerId switch
{
1uL => "Test Host",
1000uL => "Test Client 1",
2000uL => "Test Client 2",
3000uL => "Test Client 3",
_ => playerId.ToString(),
};
return false;
}
}
}
@@ -0,0 +1,82 @@
using System.Reflection;
using HarmonyLib;
using MegaCrit.Sts2.Core.Multiplayer.Game;
using MegaCrit.Sts2.Core.Multiplayer.Game.Lobby;
using MegaCrit.Sts2.Core.Platform;
using MegaCrit.Sts2.Core.Runs;
using MegaCrit.Sts2.Core.Saves;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
using SlayTheSpire2.LAN.Multiplayer.Models;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
[HarmonyPatch]
internal class RunLobbyConstructorPatchs
{
private static IEnumerable<MethodBase> TargetMethods()
{
yield return typeof(StartRunLobby).GetConstructor([
typeof(GameMode), typeof(INetGameService), typeof(IStartRunLobbyListener), typeof(int)
])!;
yield return typeof(RunLobby).GetConstructor([
typeof(GameMode), typeof(INetGameService), typeof(IRunLobbyListener), typeof(IPlayerCollection),
typeof(IEnumerable<ulong>)
])!;
yield return typeof(LoadRunLobby).GetConstructor([
typeof(INetGameService), typeof(ILoadRunLobbyListener), typeof(SerializableRun)
])!;
}
private static void Prefix(INetGameService netService)
{
if (netService.Platform == PlatformType.None)
{
LanPlayerNameHelper.NetService = netService;
netService.RegisterMessageHandler<LanPlayerNameResponseMessage>(LanPlayerNameHelper
.HandleLanPlayerNameResponseMessage);
if (netService.Type == NetGameType.Host)
{
netService.RegisterMessageHandler<LanPlayerNameRequestMessage>(LanPlayerNameHelper
.HandleLanPlayerNameRequestMessage);
}
}
}
}
[HarmonyPatch]
internal class RunLobbyCleanUpPatchs
{
private static IEnumerable<MethodBase> TargetMethods()
{
yield return typeof(StartRunLobby).GetMethod("CleanUp", BindingFlags.Instance | BindingFlags.Public)!;
yield return typeof(RunLobby).GetMethod("Dispose", BindingFlags.Instance | BindingFlags.Public)!;
yield return typeof(LoadRunLobby).GetMethod("CleanUp", BindingFlags.Instance | BindingFlags.Public)!;
}
private static void Prefix(object __instance)
{
var netService = __instance is StartRunLobby or LoadRunLobby
? Traverse.Create(__instance).Property("NetService").GetValue<INetGameService>()
: Traverse.Create(__instance).Field("_netService").GetValue<INetGameService>();
if (netService.Platform == PlatformType.None)
{
LanPlayerNameHelper.SetDefaultPlayerNameDictionary();
netService.UnregisterMessageHandler<LanPlayerNameResponseMessage>(LanPlayerNameHelper
.HandleLanPlayerNameResponseMessage);
if (netService.Type == NetGameType.Host)
{
netService.UnregisterMessageHandler<LanPlayerNameRequestMessage>(LanPlayerNameHelper
.HandleLanPlayerNameRequestMessage);
}
}
}
}
}
@@ -8,11 +8,12 @@ using SlayTheSpire2.LAN.Multiplayer.Helpers;
namespace SlayTheSpire2.LAN.Multiplayer.Patchs namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{ {
[HarmonyPatch(typeof(RunManager), "CleanUp")] [HarmonyPatch(typeof(RunManager), "CleanUp")]
internal class RunManagerPatch internal class RunManagerCleanUpPatch
{ {
private static void Postfix() private static void Postfix()
{ {
LanMapDrawingsHelper.DisableDrawingHashSet.Clear(); LanMapDrawingsHelper.DisableDrawingHashSet.Clear();
LanPlayerNameHelper.SetDefaultPlayerNameDictionary();
} }
} }
} }
@@ -8,6 +8,7 @@ using MegaCrit.Sts2.Core.Runs;
using MegaCrit.Sts2.Core.Saves; using MegaCrit.Sts2.Core.Saves;
using MegaCrit.Sts2.Core.Saves.Managers; using MegaCrit.Sts2.Core.Saves.Managers;
using SlayTheSpire2.LAN.Multiplayer.Helpers; using SlayTheSpire2.LAN.Multiplayer.Helpers;
using SlayTheSpire2.LAN.Multiplayer.Models;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
@@ -35,8 +36,12 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
return; return;
var value = RunManager.Instance.ToSave(preFinishedRoom); var value = RunManager.Instance.ToSave(preFinishedRoom);
var savePath = RunManager.Instance.NetService.Type.IsMultiplayer()
? RunManager.Instance.NetService.Platform == PlatformType.None var isMultiplayer = RunManager.Instance.NetService.Type.IsMultiplayer();
var isNonePlatform = RunManager.Instance.NetService.Platform == PlatformType.None;
var savePath = isMultiplayer
? isNonePlatform
? LanRunSaveManagerHelper.CurrentMultiplayerRunSavePath ? LanRunSaveManagerHelper.CurrentMultiplayerRunSavePath
: Traverse.Create(runSaveManager).Property("CurrentMultiplayerRunSavePath").GetValue<string>() : Traverse.Create(runSaveManager).Property("CurrentMultiplayerRunSavePath").GetValue<string>()
: Traverse.Create(runSaveManager).Property("CurrentRunSavePath").GetValue<string>(); : Traverse.Create(runSaveManager).Property("CurrentRunSavePath").GetValue<string>();
@@ -55,6 +60,25 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
stream.Seek(0L, SeekOrigin.Begin); stream.Seek(0L, SeekOrigin.Begin);
await saveStore.WriteFileAsync(savePath, stream.ToArray()); await saveStore.WriteFileAsync(savePath, stream.ToArray());
if (isMultiplayer && isNonePlatform)
{
using var playerNamesStream = new MemoryStream();
if (!forceSynchronous)
{
await JsonSerializer.SerializeAsync(playerNamesStream, LanPlayerNameHelper.PlayerNameDictionary,
LanPlayerNamesContext.Default.LanPlayerNames, CancellationToken.None);
}
else
{
await JsonSerializer.SerializeAsync(playerNamesStream, LanPlayerNameHelper.PlayerNameDictionary,
LanPlayerNamesContext.Default.LanPlayerNames);
}
playerNamesStream.Seek(0L, SeekOrigin.Begin);
await saveStore.WriteFileAsync(LanRunSaveManagerHelper.CurrentMultiplayerRunPlayerNamesPath,
playerNamesStream.ToArray());
}
saved?.Invoke(); saved?.Invoke();
} }
} }
@@ -0,0 +1,32 @@
using System.Reflection;
using HarmonyLib;
using MegaCrit.Sts2.Core.Multiplayer.Game;
using MegaCrit.Sts2.Core.Nodes.Screens.CharacterSelect;
using MegaCrit.Sts2.Core.Platform;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{
[HarmonyPatch]
internal class NCharacterSelectScreenInitializeMultiplayerPatchs
{
private static IEnumerable<MethodInfo> TargetMethods()
{
yield return typeof(NCharacterSelectScreen).GetMethod("InitializeMultiplayerAsClient",
BindingFlags.Instance | BindingFlags.Public)!;
yield return typeof(NCharacterSelectScreen).GetMethod("InitializeMultiplayerAsHost",
BindingFlags.Instance | BindingFlags.Public)!;
}
private static void Prefix(NCharacterSelectScreen __instance, INetGameService gameService)
{
if (gameService.Platform == PlatformType.None)
{
LanPlayerNameHelper.CharacterSelectScreen = __instance;
}
}
}
}
@@ -12,10 +12,10 @@ using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{ {
[HarmonyPatch(typeof(NJoinFriendScreen), "_Ready")] [HarmonyPatch(typeof(NJoinFriendScreen), "_Ready")]
internal class NJoinFriendScreenPatch internal class NJoinFriendScreenReadyPatch
{ {
private static void Prefix(NJoinFriendScreen __instance) private static void Prefix(NJoinFriendScreen __instance)
{ {
@@ -28,11 +28,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
lanPanel.PatchMarginLeft = 12; lanPanel.PatchMarginLeft = 12;
lanPanel.PatchMarginRight = 12; lanPanel.PatchMarginRight = 12;
lanPanel.AnchorLeft = 0.5f; lanPanel.SetAnchorsPreset(Control.LayoutPreset.Center);
lanPanel.AnchorTop = 0.5f;
lanPanel.AnchorRight = 0.5f;
lanPanel.AnchorBottom = 0.5f;
lanPanel.OffsetLeft = 450; lanPanel.OffsetLeft = 450;
lanPanel.OffsetTop = -338; lanPanel.OffsetTop = -338;
lanPanel.OffsetRight = 790; lanPanel.OffsetRight = 790;
@@ -49,8 +45,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
lanPanel.AddChild(vBoxContainer); lanPanel.AddChild(vBoxContainer);
vBoxContainer.Alignment = BoxContainer.AlignmentMode.Center; vBoxContainer.Alignment = BoxContainer.AlignmentMode.Center;
vBoxContainer.SetAnchorsPreset(Control.LayoutPreset.FullRect); vBoxContainer.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
vBoxContainer.Size = new Vector2(350, 676);
vBoxContainer.AddThemeConstantOverride("separation", 24); vBoxContainer.AddThemeConstantOverride("separation", 24);
if (__instance.GetNode("TitleLabel").Duplicate() is MegaLabel ipAddressLabel) if (__instance.GetNode("TitleLabel").Duplicate() is MegaLabel ipAddressLabel)
@@ -7,10 +7,10 @@ using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{ {
[HarmonyPatch(typeof(NMapDrawings), "CreateLineForPlayer")] [HarmonyPatch(typeof(NMapDrawings), "CreateLineForPlayer")]
internal class NMapDrawingsPatch internal class NMapDrawingsCreateLineForPlayerPatch
{ {
private static void Postfix(Player player, Line2D __result) private static void Postfix(Player player, Line2D __result)
{ {
@@ -0,0 +1,32 @@
using System.Reflection;
using HarmonyLib;
using MegaCrit.Sts2.Core.Multiplayer.Game;
using MegaCrit.Sts2.Core.Nodes.Screens.CharacterSelect;
using MegaCrit.Sts2.Core.Platform;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{
[HarmonyPatch]
internal class NMultiplayerLoadGameScreenInitializePatchs
{
private static IEnumerable<MethodInfo> TargetMethods()
{
yield return typeof(NMultiplayerLoadGameScreen).GetMethod("InitializeAsHost",
BindingFlags.Instance | BindingFlags.Public)!;
yield return typeof(NMultiplayerLoadGameScreen).GetMethod("InitializeAsClient",
BindingFlags.Instance | BindingFlags.Public)!;
}
private static void Prefix(NMultiplayerLoadGameScreen __instance, INetGameService gameService)
{
if (gameService.Platform == PlatformType.None)
{
LanPlayerNameHelper.MultiplayerLoadGameScreen = __instance;
}
}
}
}
@@ -12,10 +12,10 @@ using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{ {
[HarmonyPatch(typeof(NMultiplayerPlayerExpandedState), "_Ready")] [HarmonyPatch(typeof(NMultiplayerPlayerExpandedState), "_Ready")]
internal class NMultiplayerPlayerExpandedStatePatch internal class NMultiplayerPlayerExpandedStateReadyPatch
{ {
private static void Prefix(NMultiplayerPlayerExpandedState __instance, Player ____player) private static void Prefix(NMultiplayerPlayerExpandedState __instance, Player ____player)
{ {
@@ -10,7 +10,7 @@ using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{ {
[HarmonyPatch(typeof(NMultiplayerSubmenu), "_Ready")] [HarmonyPatch(typeof(NMultiplayerSubmenu), "_Ready")]
internal class NMultiplayerSubmenuReadyPatch internal class NMultiplayerSubmenuReadyPatch
@@ -6,10 +6,10 @@ using MegaCrit.Sts2.Core.Runs;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{ {
[HarmonyPatch(typeof(NRestSiteRoom), "_Ready")] [HarmonyPatch(typeof(NRestSiteRoom), "_Ready")]
internal class NRestSiteRoomPatch internal class NRestSiteRoomReadyPatch
{ {
private static void Prefix(NRestSiteRoom __instance, IRunState ____runState, private static void Prefix(NRestSiteRoom __instance, IRunState ____runState,
List<Control> ____characterContainers) List<Control> ____characterContainers)
@@ -1,21 +1,23 @@
using Godot; using Godot;
using HarmonyLib; using HarmonyLib;
using MegaCrit.Sts2.Core.Nodes.Screens.Settings; using MegaCrit.Sts2.Core.Nodes.Screens.Settings;
using SlayTheSpire2.LAN.Multiplayer.Components;
using SlayTheSpire2.LAN.Multiplayer.Helpers; using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{ {
[HarmonyPatch(typeof(NSettingsScreen), "_Ready")] [HarmonyPatch(typeof(NSettingsScreen), "_Ready")]
internal class NSettingsScreenPatch internal class NSettingsScreenReadyPatch
{ {
private static void Prefix(NSettingsScreen __instance) private static void Prefix(NSettingsScreen __instance)
{ {
var moddingNode = __instance.GetNode("%Modding"); var moddingNode = __instance.GetNode("%Modding");
var vBoxContainer = moddingNode.GetParent(); var vBoxContainerNode = moddingNode.GetParent();
var generalSettings = vBoxContainerNode.GetParent();
if (__instance.GetNode("%ModdingDivider").Duplicate() is ColorRect hostPortDivider && if (__instance.GetNode("%ModdingDivider").Duplicate() is ColorRect hostPortDivider &&
moddingNode.Duplicate() is MarginContainer hostPort && moddingNode.Duplicate() is MarginContainer hostPort &&
@@ -23,8 +25,8 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{ {
hostPortDivider.Name = "HostPortDivider"; hostPortDivider.Name = "HostPortDivider";
vBoxContainer.AddChild(hostPortDivider); vBoxContainerNode.AddChild(hostPortDivider);
vBoxContainer.MoveChild(hostPortDivider, moddingNode.GetIndex() + 1); vBoxContainerNode.MoveChild(hostPortDivider, moddingNode.GetIndex() + 1);
hostPortDivider.Show(); hostPortDivider.Show();
@@ -32,8 +34,8 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
hostPort.RemoveChild(hostPort.GetNode("ModdingButton")); hostPort.RemoveChild(hostPort.GetNode("ModdingButton"));
vBoxContainer.AddChild(hostPort); vBoxContainerNode.AddChild(hostPort);
vBoxContainer.MoveChild(hostPort, hostPortDivider.GetIndex() + 1); vBoxContainerNode.MoveChild(hostPort, hostPortDivider.GetIndex() + 1);
hostPort.Show(); hostPort.Show();
@@ -64,8 +66,8 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{ {
hostMaxPlayersDivider.Name = "HostMaxPlayersDivider"; hostMaxPlayersDivider.Name = "HostMaxPlayersDivider";
vBoxContainer.AddChild(hostMaxPlayersDivider); vBoxContainerNode.AddChild(hostMaxPlayersDivider);
vBoxContainer.MoveChild(hostMaxPlayersDivider, moddingNode.GetIndex() + 1); vBoxContainerNode.MoveChild(hostMaxPlayersDivider, moddingNode.GetIndex() + 1);
hostMaxPlayersDivider.Show(); hostMaxPlayersDivider.Show();
@@ -73,8 +75,8 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
hostMaxPlayers.RemoveChild(hostMaxPlayers.GetNode("ModdingButton")); hostMaxPlayers.RemoveChild(hostMaxPlayers.GetNode("ModdingButton"));
vBoxContainer.AddChild(hostMaxPlayers); vBoxContainerNode.AddChild(hostMaxPlayers);
vBoxContainer.MoveChild(hostMaxPlayers, hostMaxPlayersDivider.GetIndex() + 1); vBoxContainerNode.MoveChild(hostMaxPlayers, hostMaxPlayersDivider.GetIndex() + 1);
hostMaxPlayers.Show(); hostMaxPlayers.Show();
@@ -98,14 +100,57 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
hostMaxPlayersLabel.Text = "Host Max Players"; hostMaxPlayersLabel.Text = "Host Max Players";
} }
if (__instance.GetNode("%ModdingDivider").Duplicate() is ColorRect playerNameDivider &&
moddingNode.Duplicate() is MarginContainer playerName &&
playerName.GetNode("Label") is RichTextLabel playerNameLabel)
{
playerNameDivider.Name = "PlayerNameDivider";
vBoxContainerNode.AddChild(playerNameDivider);
vBoxContainerNode.MoveChild(playerNameDivider, moddingNode.GetIndex() + 1);
playerNameDivider.Show();
playerName.Name = "PlayerName";
playerName.RemoveChild(playerName.GetNode("ModdingButton"));
vBoxContainerNode.AddChild(playerName);
vBoxContainerNode.MoveChild(playerName, playerNameDivider.GetIndex() + 1);
playerName.Show();
var playerNameInput = new PlayerNameLineEdit { Name = "PlayerNameInput" };
playerName.AddChild(playerNameInput);
playerNameInput.CustomMinimumSize = new Vector2(324, 64);
playerNameInput.SizeFlagsHorizontal = Control.SizeFlags.ShrinkEnd;
playerNameInput.Alignment = HorizontalAlignment.Center;
playerNameInput.MaxLength = 16;
playerNameInput.Text = SettingsHelper.Instance.SettingsModel.PlayerName;
playerNameInput.TextChanged += value =>
{
if (playerNameInput.IsEmpty || !playerNameInput.IsInvalid)
{
SettingsHelper.Instance.SettingsModel.PlayerName = value;
LanPlayerNameHelper.SetHostPlayerName();
SettingsHelper.Instance.WriteSettings();
}
};
playerNameLabel.Text = "Player Name";
}
if (__instance.GetNode("%ModdingDivider").Duplicate() is ColorRect netIdDivider && if (__instance.GetNode("%ModdingDivider").Duplicate() is ColorRect netIdDivider &&
moddingNode.Duplicate() is MarginContainer netId && moddingNode.Duplicate() is MarginContainer netId &&
netId.GetNode("Label") is RichTextLabel netIdLabel) netId.GetNode("Label") is RichTextLabel netIdLabel)
{ {
netIdDivider.Name = "NetIDDivider"; netIdDivider.Name = "NetIDDivider";
vBoxContainer.AddChild(netIdDivider); vBoxContainerNode.AddChild(netIdDivider);
vBoxContainer.MoveChild(netIdDivider, moddingNode.GetIndex() + 1); vBoxContainerNode.MoveChild(netIdDivider, moddingNode.GetIndex() + 1);
netIdDivider.Show(); netIdDivider.Show();
@@ -113,8 +158,8 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
netId.RemoveChild(netId.GetNode("ModdingButton")); netId.RemoveChild(netId.GetNode("ModdingButton"));
vBoxContainer.AddChild(netId); vBoxContainerNode.AddChild(netId);
vBoxContainer.MoveChild(netId, netIdDivider.GetIndex() + 1); vBoxContainerNode.MoveChild(netId, netIdDivider.GetIndex() + 1);
netId.Show(); netId.Show();
@@ -138,6 +183,11 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
netIdLabel.Text = "NetID"; netIdLabel.Text = "NetID";
} }
if (generalSettings is NSettingsPanel nSettingsPanel)
{
Traverse.Create(nSettingsPanel).Method("RefreshSize").GetValue();
}
} }
} }
} }
@@ -0,0 +1,122 @@
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.Core.Assets;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Nodes.GodotExtensions;
using MegaCrit.Sts2.Core.Nodes.Screens.TreasureRoomRelic;
using MegaCrit.Sts2.Core.Runs;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{
[HarmonyPatch(typeof(NTreasureRoomRelicCollection), "_Ready")]
internal class NTreasureRoomRelicCollectionReadyPatch
{
private static void Prefix(NTreasureRoomRelicCollection __instance)
{
var runState = Traverse.Create(RunManager.Instance).Property("State").GetValue<RunState?>();
if (runState is { Players.Count: > 4 })
{
var container = __instance.GetNode("Container");
var multiplayerRelicHolder = container.GetNode("MultiplayerRelicHolder4");
for (var i = 4; i < runState.Players.Count; i++)
{
var newMultiplayerRelicHolder = multiplayerRelicHolder.Duplicate();
container.AddChild(newMultiplayerRelicHolder);
if (newMultiplayerRelicHolder is NTreasureRoomRelicHolder nTreasureRoomRelicHolder)
{
nTreasureRoomRelicHolder.Name = $"MultiplayerRelicHolder{i + 1}";
}
}
}
}
private static void Postfix(NTreasureRoomRelicCollection __instance,
List<NTreasureRoomRelicHolder> ____multiplayerHolders)
{
if (____multiplayerHolders.Count > 4)
{
var container = __instance.GetNode("Container");
var scrollContainer = new NScrollableContainer { Name = "ScrollableContainer" };
var mask = new TextureRect { Name = "Mask" };
scrollContainer.AddChild(mask);
mask.ClipContents = true;
var viewport = new Control { Name = "Viewport" };
mask.AddChild(viewport);
var gridContainer = new GridContainer { Name = "RelicContainer" };
viewport.AddChild(gridContainer);
var scrollbar = PreloadManager.Cache.GetScene(SceneHelper.GetScenePath("ui/scrollbar"))
.Instantiate<NScrollbar>();
scrollContainer.AddChild(scrollbar);
container.AddChild(scrollContainer);
scrollContainer.SetAnchorsPreset(Control.LayoutPreset.FullRect);
scrollContainer.OffsetLeft = -510;
scrollContainer.OffsetTop = -250;
scrollContainer.OffsetRight = 510;
scrollContainer.OffsetBottom = 250;
mask.SetAnchorsPreset(Control.LayoutPreset.CenterTop);
mask.OffsetLeft = -450;
mask.OffsetTop = 250;
mask.OffsetRight = 450;
mask.OffsetBottom = 830;
viewport.SetAnchorsPreset(Control.LayoutPreset.TopWide);
viewport.OffsetLeft = 110;
viewport.OffsetTop = 110;
viewport.OffsetRight = -120;
viewport.OffsetBottom = 580;
scrollbar.AnchorLeft = 0.794f;
scrollbar.AnchorTop = 0.187f;
scrollbar.AnchorRight = 0.818f;
scrollbar.AnchorBottom = 0.947f;
scrollbar.OffsetLeft = -200;
scrollbar.OffsetTop = 158;
scrollbar.OffsetRight = -199;
scrollbar.OffsetBottom = -163;
gridContainer.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.TopWide);
gridContainer.Columns = 4;
gridContainer.CustomMinimumSize = new Vector2(0, 469);
gridContainer.AddThemeConstantOverride("h_separation", 42);
gridContainer.AddThemeConstantOverride("v_separation", 42);
scrollContainer.SetContent(gridContainer, 20, 30);
//The animation will change MultiplayerHolder position. Manually fix the position.
var position = new Vector2(0, 110);
for (var i = 0; i < ____multiplayerHolders.Count; i++)
{
if (i > 0)
{
position = i % 4 == 0
? new Vector2(0, position.Y + 178)
: new Vector2(position.X + 178, position.Y);
}
var multiplayerHolder = ____multiplayerHolders[i];
multiplayerHolder.Position = position;
multiplayerHolder.Reparent(gridContainer, false);
}
}
}
}
}