Added Multilingual Localization

Added Host IP Information Panel

Used 7-bit variant to expand the playersInLobby list and slotId of LobbyMessage

Fixed LAN UI was not displayed after switch save
Fixed where last max player is very difficult to connect
Fixed Join button on the controller is the same as the refresh button
This commit is contained in:
kmyuhkyuk
2026-03-22 16:04:20 +08:00
parent 1a35bc7894
commit 9e7c3ea567
53 changed files with 1514 additions and 164 deletions
@@ -4,10 +4,11 @@ using MegaCrit.Sts2.Core.Nodes.GodotExtensions;
namespace SlayTheSpire2.LAN.Multiplayer.Components
{
internal class AddressLineEdit : NMegaLineEdit
internal partial class AddressLineEdit : NMegaLineEdit
{
public override void _Ready()
{
base._Ready();
TextChanged += OnTextChanged;
}
@@ -0,0 +1,70 @@
using Godot;
using MegaCrit.Sts2.addons.mega_text;
using MegaCrit.Sts2.Core.Localization;
using MegaCrit.Sts2.Core.Localization.Fonts;
namespace SlayTheSpire2.LAN.Multiplayer.Components
{
internal partial class CopiedLabel : MegaLabel
{
private Tween? _tween;
private const string LocKeyPrefix = "SlayTheSpire2.LAN.Multiplayer.COPIED";
public override void _Ready()
{
Modulate = new Color(Colors.White, 0);
AutoSizeEnabled = false;
MinFontSize = 24;
AddThemeColorOverride("font_color", new Color(1.0f, 0.922f, 0.761f));
AddThemeColorOverride("font_shadow_color", new Color(Colors.Black, 0.251f));
var font = GD.Load<Font>("res://themes/kreon_bold_glyph_space_one.tres");
AddThemeFontOverride("font", font);
AddThemeFontSizeOverride("font_size", 23);
RefreshLabel();
base._Ready();
}
public void ShowWithPosition(Vector2 position)
{
_tween?.Kill();
Modulate = new Color(Colors.White);
GlobalPosition = position;
_tween = GetTree().CreateTween();
_tween.SetParallel();
_tween.TweenProperty(this, "position:y", Position.Y - 30, 0.3f).SetTrans(Tween.TransitionType.Cubic)
.SetEase(Tween.EaseType.Out);
_tween.Chain().TweenProperty(this, "modulate:a", 0, 0.4f).SetDelay(0.8f);
}
public override void _Notification(int what)
{
if ((long)what == 2010 && IsNodeReady())
{
RefreshLabel();
}
base._Notification(what);
}
private void RefreshLabel()
{
var locString = new LocString("main_menu_ui", LocKeyPrefix);
SetTextAutoSize(locString.GetFormattedText());
this.ApplyLocaleFontSubstitution(FontType.Regular, ThemeConstants.Label.font);
}
}
}
@@ -0,0 +1,474 @@
using System.Net.NetworkInformation;
using System.Net.Sockets;
using Godot;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Nodes.CommonUi;
using SlayTheSpire2.LAN.Multiplayer.Services;
using BoxContainer = Godot.BoxContainer;
using Control = Godot.Control;
using HttpClient = System.Net.Http.HttpClient;
namespace SlayTheSpire2.LAN.Multiplayer.Components
{
internal partial class IPAddressInfoPanel : Control
{
private Control? _content;
private Control? _menu;
private Control? _box;
private Control? _loading;
private CopiedLabel? _copiedLabel;
private Control? _ipAddress;
private IPAddressLabel? _ipAddressTitleLabel;
private IPAddressLabel? _ipAddressLabel;
private Control? _ipv6Address;
private IPAddressLabel? _ipv6AddressTitleLabel;
private IPAddressLabel? _ipv6AddressLabel;
private Control? _localIPAddress;
private IPAddressLabel? _localIPAddressTitleLabel;
private Control? _localIPAddressContainer;
private CancellationTokenSource? _cancellationTokenSource;
private static readonly HttpClient HttpClient = new();
public static IPAddressInfoPanel Create()
{
var ipAddressInfoPanel = new IPAddressInfoPanel();
ipAddressInfoPanel.MouseFilter = MouseFilterEnum.Ignore;
ipAddressInfoPanel.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
var content = new VBoxContainer { Name = "Content" };
ipAddressInfoPanel.AddChild(content);
content.MouseFilter = MouseFilterEnum.Stop;
var menu = new Control { Name = "Menu" };
content.AddChild(menu);
menu.CustomMinimumSize = new Vector2(300, 24);
menu.MouseFilter = MouseFilterEnum.Pass;
var background = new NinePatchRect { Name = "Background" };
menu.AddChild(background);
background.MouseFilter = MouseFilterEnum.Ignore;
background.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
background.Texture = GD.Load<CompressedTexture2D>("res://images/ui/tiny_nine_patch.png");
background.PatchMarginLeft = 12;
background.PatchMarginTop = 12;
background.PatchMarginRight = 12;
background.PatchMarginBottom = 12;
background.Modulate = new Color(Colors.Black, 0.471f);
var menuIcon = new TextureRect { Name = "Icon" };
menu.AddChild(menuIcon);
menuIcon.MouseFilter = MouseFilterEnum.Ignore;
menuIcon.SetAnchorsPreset(LayoutPreset.Center);
menuIcon.OffsetLeft = -12;
menuIcon.OffsetTop = -12;
menuIcon.OffsetRight = 12;
menuIcon.OffsetBottom = 12;
var menuSvgImage = new Image();
menuSvgImage.LoadSvgFromString(
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" fill=\"#FFFFFF\"><path d=\"M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z\"/></svg>");
menuIcon.Texture = ImageTexture.CreateFromImage(menuSvgImage);
var box = new PanelContainer { Name = "Box" };
content.AddChild(box);
box.MouseFilter = MouseFilterEnum.Ignore;
box.Modulate = new Color(Colors.White, 0);
var styleBox = new StyleBoxTexture();
styleBox.Texture = GD.Load<CompressedTexture2D>("res://images/ui/tiny_nine_patch.png");
styleBox.TextureMarginLeft = 12;
styleBox.TextureMarginTop = 12;
styleBox.TextureMarginRight = 12;
styleBox.TextureMarginBottom = 12;
styleBox.ContentMarginLeft = 12;
styleBox.ContentMarginTop = 12;
styleBox.ContentMarginRight = 12;
styleBox.ContentMarginBottom = 12;
styleBox.ModulateColor = new Color(Colors.Black, 0.471f);
box.AddThemeStyleboxOverride("panel", styleBox);
var container = new VBoxContainer { Name = "Container" };
box.AddChild(container);
container.MouseFilter = MouseFilterEnum.Ignore;
container.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
container.Alignment = BoxContainer.AlignmentMode.Center;
container.AddThemeConstantOverride("separation", 8);
var loading = new Control { Name = "Loading" };
container.AddChild(loading);
loading.CustomMinimumSize = new Vector2(64, 64);
loading.MouseFilter = MouseFilterEnum.Ignore;
var loadingIcon = new LoadingIcon();
loading.AddChild(loadingIcon);
loadingIcon.MouseFilter = MouseFilterEnum.Ignore;
loadingIcon.SetAnchorsPreset(LayoutPreset.Center);
loadingIcon.OffsetLeft = -32;
loadingIcon.OffsetTop = -32;
loadingIcon.OffsetRight = 32;
loadingIcon.OffsetBottom = 32;
AddAddressElement(container, "IPAddress", "SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE", false);
AddAddressElement(container, "IPV6IPAddress", "SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE", true);
AddAddressContainer(container, "LocalIPAddress", "SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE");
content.SetAnchorsAndOffsetsPreset(LayoutPreset.CenterTop);
var copiedLabel = new CopiedLabel { Name = "CopiedLabel" };
ipAddressInfoPanel.AddChild(copiedLabel);
copiedLabel.MouseFilter = MouseFilterEnum.Ignore;
return ipAddressInfoPanel;
}
private static void AddAddressElement(Node container, string name, string locKeyPrefix, bool isTrim)
{
var addressElement = new HBoxContainer { Name = name };
container.AddChild(addressElement);
addressElement.CustomMinimumSize = new Vector2(0, 24);
addressElement.Alignment = BoxContainer.AlignmentMode.Center;
addressElement.MouseFilter = MouseFilterEnum.Ignore;
var ipAddressTitleLabel = new IPAddressLabel { Name = "TitleLabel" };
addressElement.AddChild(ipAddressTitleLabel);
ipAddressTitleLabel.MouseFilter = MouseFilterEnum.Ignore;
ipAddressTitleLabel.SetLocalization(locKeyPrefix);
var ipAddressLabel = new IPAddressLabel { Name = "Label" };
addressElement.AddChild(ipAddressLabel);
ipAddressLabel.MouseFilter = MouseFilterEnum.Ignore;
if (isTrim)
{
ipAddressLabel.SizeFlagsHorizontal = SizeFlags.ExpandFill;
ipAddressLabel.TextOverrunBehavior = TextServer.OverrunBehavior.TrimEllipsis;
}
}
private static void AddAddressContainer(Node container, string name, string locKeyPrefix)
{
var addressElement = new VBoxContainer { Name = name };
container.AddChild(addressElement);
addressElement.CustomMinimumSize = new Vector2(0, 24);
addressElement.MouseFilter = MouseFilterEnum.Ignore;
var ipAddressTitleLabel = new IPAddressLabel { Name = "TitleLabel" };
addressElement.AddChild(ipAddressTitleLabel);
ipAddressTitleLabel.HorizontalAlignment = HorizontalAlignment.Center;
ipAddressTitleLabel.SetLocalization(locKeyPrefix);
var vBoxContainer = new VBoxContainer { Name = "Container" };
addressElement.AddChild(vBoxContainer);
vBoxContainer.MouseFilter = MouseFilterEnum.Ignore;
vBoxContainer.Alignment = BoxContainer.AlignmentMode.Center;
}
public override void _Ready()
{
_content = GetNode<Control>("Content");
_menu = GetNode<Control>("Content/Menu");
_box = GetNode<Control>("Content/Box");
_loading = GetNode<Control>("Content/Box/Container/Loading");
_ipAddress = GetNode<Control>("Content/Box/Container/IPAddress");
_ipAddressTitleLabel = _ipAddress.GetNode<IPAddressLabel>("TitleLabel");
_ipAddressLabel = _ipAddress.GetNode<IPAddressLabel>("Label");
_copiedLabel = GetNode<CopiedLabel>("CopiedLabel");
_ipAddress.GuiInput += inputEvent =>
{
if (inputEvent is InputEventMouseButton
{
ButtonIndex: MouseButton.Left, Pressed: true
} inputEventMouseButton &&
!string.IsNullOrEmpty(_ipAddressLabel.Text))
{
DisplayServer.ClipboardSet(_ipAddressLabel.Text);
_copiedLabel.ShowWithPosition(inputEventMouseButton.GlobalPosition);
}
};
_ipv6Address = GetNode<Control>("Content/Box/Container/IPV6IPAddress");
_ipv6AddressTitleLabel = _ipv6Address.GetNode<IPAddressLabel>("TitleLabel");
_ipv6AddressLabel = _ipv6Address.GetNode<IPAddressLabel>("Label");
_ipv6Address.GuiInput += inputEvent =>
{
if (inputEvent is InputEventMouseButton
{
ButtonIndex: MouseButton.Left, Pressed: true
} inputEventMouseButton &&
!string.IsNullOrEmpty(_ipv6AddressLabel.Text))
{
DisplayServer.ClipboardSet(_ipv6AddressLabel.Text);
_copiedLabel.ShowWithPosition(inputEventMouseButton.GlobalPosition);
}
};
_localIPAddress = GetNode<Control>("Content/Box/Container/LocalIPAddress");
_localIPAddressTitleLabel = _localIPAddress.GetNode<IPAddressLabel>("TitleLabel");
_localIPAddressContainer = _localIPAddress.GetNode<Control>("Container");
UpdateController();
_menu.MouseEntered += OnMouseEntered;
_content.MouseExited += OnMouseExited;
NControllerManager.Instance?.Connect(NControllerManager.SignalName.MouseDetected,
Callable.From(UpdateController));
NControllerManager.Instance?.Connect(NControllerManager.SignalName.ControllerDetected,
Callable.From(UpdateController));
NInputManager.Instance?.Connect(NInputManager.SignalName.InputRebound, Callable.From(UpdateController));
}
private void UpdateController()
{
if (NControllerManager.Instance?.IsUsingController ?? false)
{
ShowBox();
}
}
public void Initialize()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
TaskHelper.RunSafely(InitializeAsync(_cancellationTokenSource.Token));
}
private async Task InitializeAsync(CancellationToken cancellationToken)
{
if (_ipAddress == null || _localIPAddress == null || _ipv6Address == null || _loading == null ||
_content == null || _localIPAddressContainer == null)
{
Log.Error($"{nameof(IPAddressInfoPanel)} has null element");
return;
}
_ipAddress.Visible = false;
_localIPAddress.Visible = false;
_ipv6Address.Visible = false;
foreach (var child in _localIPAddressContainer.GetChildren())
{
child.QueueFree();
}
_loading.Visible = true;
_content.Size = _content.CustomMinimumSize;
var ipAddress = string.Empty;
var port = SettingsService.Instance.SettingsModel.HostPort;
var hasIPAddress = false;
try
{
ipAddress = await HttpClient.GetStringAsync("https://api-ipv4.ip.sb/ip", cancellationToken);
if (!string.IsNullOrEmpty(ipAddress))
{
_ipAddressLabel?.SetTextAutoSize($"{ipAddress.Replace("\n", string.Empty)}:{port}");
hasIPAddress = true;
}
}
catch (OperationCanceledException ex)
{
Log.Debug(ex.Message);
}
catch (Exception ex)
{
Log.Warn(ex.Message);
}
var hasLocalIPAddress = false;
var localIPAddressList = GetLocalIPAddressList();
foreach (var localIPAddress in localIPAddressList)
{
if (localIPAddress != ipAddress)
{
var ipAddressLabel = new IPAddressLabel();
_localIPAddressContainer.AddChild(ipAddressLabel);
ipAddressLabel.MouseFilter = MouseFilterEnum.Pass;
ipAddressLabel.HorizontalAlignment = HorizontalAlignment.Center;
ipAddressLabel.Text = $"{localIPAddress}:{port}";
ipAddressLabel.GuiInput += inputEvent =>
{
if (inputEvent is InputEventMouseButton
{
ButtonIndex: MouseButton.Left, Pressed: true
} inputEventMouseButton &&
!string.IsNullOrEmpty(ipAddressLabel.Text))
{
DisplayServer.ClipboardSet(ipAddressLabel.Text);
_copiedLabel?.ShowWithPosition(inputEventMouseButton.GlobalPosition);
}
};
hasLocalIPAddress = true;
}
}
var hasIPV6Address = false;
try
{
var ipv6Address = await HttpClient.GetStringAsync("https://api-ipv6.ip.sb/ip", cancellationToken);
_ipv6AddressLabel?.SetTextAutoSize($"[{ipv6Address.Replace("\n", string.Empty)}]:{port}");
hasIPV6Address = true;
}
catch (OperationCanceledException ex)
{
Log.Debug(ex.Message);
}
catch (Exception ex)
{
Log.Warn(ex.Message);
}
_loading.Visible = false;
_ipAddress.Visible = hasIPAddress;
_localIPAddress.Visible = hasLocalIPAddress;
_ipv6Address.Visible = hasIPV6Address;
_content.SetAnchorsAndOffsetsPreset(LayoutPreset.CenterTop);
}
private static List<string> GetLocalIPAddressList()
{
var list = new List<string>();
foreach (var networkInterface in NetworkInterface.GetAllNetworkInterfaces())
{
if (networkInterface.OperationalStatus == OperationalStatus.Up &&
networkInterface.NetworkInterfaceType != NetworkInterfaceType.Loopback)
{
if (networkInterface.Name.Contains("vEthernet"))
continue;
foreach (var ip in networkInterface.GetIPProperties().UnicastAddresses)
{
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
{
list.Add(ip.Address.ToString());
}
}
}
}
return list;
}
private void OnMouseEntered()
{
if (NControllerManager.Instance?.IsUsingController ?? false)
return;
ShowBox();
}
private void OnMouseExited()
{
if (NControllerManager.Instance?.IsUsingController ?? false)
return;
HideBox();
}
private void ShowBox()
{
if (_box == null || _ipAddress == null || _localIPAddress == null || _ipv6Address == null)
return;
_box.Modulate = new Color(Colors.White);
_ipAddress.MouseFilter = MouseFilterEnum.Pass;
_localIPAddress.MouseFilter = MouseFilterEnum.Pass;
_ipv6Address.MouseFilter = MouseFilterEnum.Pass;
}
private void HideBox()
{
if (_box == null || _ipAddress == null || _localIPAddress == null || _ipv6Address == null)
return;
_box.Modulate = new Color(Colors.White, 0);
_ipAddress.MouseFilter = MouseFilterEnum.Ignore;
_localIPAddress.MouseFilter = MouseFilterEnum.Ignore;
_ipv6Address.MouseFilter = MouseFilterEnum.Ignore;
}
public override void _ExitTree()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
}
}
}
@@ -0,0 +1,55 @@
using Godot;
using MegaCrit.Sts2.addons.mega_text;
using MegaCrit.Sts2.Core.Localization;
using MegaCrit.Sts2.Core.Localization.Fonts;
namespace SlayTheSpire2.LAN.Multiplayer.Components
{
internal partial class IPAddressLabel : MegaLabel
{
private string? _locKeyPrefix;
public override void _Ready()
{
AutoSizeEnabled = false;
MinFontSize = 24;
AddThemeColorOverride("font_color", new Color(1.0f, 0.922f, 0.761f));
AddThemeColorOverride("font_shadow_color", new Color(Colors.Black, 0.251f));
var font = GD.Load<Font>("res://themes/kreon_bold_glyph_space_one.tres");
AddThemeFontOverride("font", font);
AddThemeFontSizeOverride("font_size", 23);
base._Ready();
}
public override void _Notification(int what)
{
if ((long)what == 2010 && IsNodeReady())
{
RefreshLabel();
}
base._Notification(what);
}
public void SetLocalization(string locKeyPrefix)
{
_locKeyPrefix = locKeyPrefix;
RefreshLabel();
}
private void RefreshLabel()
{
if (_locKeyPrefix != null)
{
var locString = new LocString("main_menu_ui", _locKeyPrefix);
SetTextAutoSize(locString.GetFormattedText());
this.ApplyLocaleFontSubstitution(FontType.Regular, ThemeConstants.Label.font);
}
}
}
}
@@ -0,0 +1,62 @@
using Godot;
using MegaCrit.Sts2.addons.mega_text;
using MegaCrit.Sts2.Core.ControllerInput;
using MegaCrit.Sts2.Core.Localization;
using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu;
namespace SlayTheSpire2.LAN.Multiplayer.Components
{
internal partial class JoinButton : NJoinFriendRefreshButton
{
protected override string[] Hotkeys => [MegaInput.viewMap];
public static JoinButton Create(NJoinFriendRefreshButton joinFriendRefreshButton)
{
var joinButton = new JoinButton();
joinButton.CustomMinimumSize = new Vector2(150, 50);
joinButton.SizeFlagsHorizontal = SizeFlags.ShrinkCenter;
joinButton.MouseFilter = MouseFilterEnum.Stop;
joinButton.Material = joinFriendRefreshButton.Material.Duplicate() as Material;
var background = new NinePatchRect { Name = "Background" };
joinButton.AddChild(background);
background.MouseFilter = MouseFilterEnum.Ignore;
background.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
background.Texture = GD.Load<CompressedTexture2D>("res://images/ui/tiny_nine_patch.png");
background.PatchMarginLeft = 12;
background.PatchMarginTop = 12;
background.PatchMarginRight = 12;
background.PatchMarginBottom = 12;
background.Modulate = joinFriendRefreshButton.SelfModulate;
background.Material = joinButton.Material;
foreach (var child in joinFriendRefreshButton.GetChildren())
{
joinButton.AddChild(child.Duplicate());
}
var controllerIcon = joinButton.GetNode<TextureRect>("ControllerIcon");
controllerIcon.Owner = joinButton;
controllerIcon.Position = new Vector2(controllerIcon.Position.X - 12, controllerIcon.Position.Y);
return joinButton;
}
public override void _Ready()
{
base._Ready();
var node = GetNode<MegaLabel>("Label");
node.SetTextAutoSize(new LocString("main_menu_ui", "JOIN.title").GetFormattedText());
}
}
}
@@ -11,7 +11,7 @@ using SlayTheSpire2.LAN.Multiplayer.Services;
namespace SlayTheSpire2.LAN.Multiplayer.Components
{
internal class LanMultiplayerHostSubmenu : NMultiplayerHostSubmenu
internal partial class LanMultiplayerHostSubmenu : NMultiplayerHostSubmenu
{
private static readonly string ScenePath = SceneHelper.GetScenePath("screens/multiplayer_host_submenu");
@@ -21,7 +21,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Components
public new static NMultiplayerHostSubmenu? Create()
{
if (Instance != null)
if (IsInstanceValid(Instance))
return Instance;
if (TestMode.IsOn)
@@ -0,0 +1,25 @@
using Godot;
namespace SlayTheSpire2.LAN.Multiplayer.Components
{
internal partial class LoadingIcon : TextureRect
{
private Tween? _tween;
public override void _Ready()
{
var svgImage = new Image();
svgImage.LoadSvgFromString(
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"64px\" viewBox=\"0 -960 960 960\" width=\"64px\" fill=\"#FFFFFF\"><path d=\"M323-111q-73-31-127-85t-85-127q-31-73-31-157t31-157q31-73 85-127t127-85q73-31 157-31 12 0 21 9t9 21q0 12-9 21t-21 9q-141 0-240.5 99.5T140-480q0 141 99.5 240.5T480-140q141 0 240.5-99.5T820-480q0-12 9-21t21-9q12 0 21 9t9 21q0 84-31 157t-85 127q-54 54-127 85T480-80q-84 0-157-31Z\"/></svg>");
Texture = ImageTexture.CreateFromImage(svgImage);
PivotOffset = Size / 2;
_tween = CreateTween().SetLoops();
_tween.TweenProperty(this, "rotation", Mathf.DegToRad(360), 2f).AsRelative();
}
}
}
@@ -13,8 +13,9 @@ namespace SlayTheSpire2.LAN.Multiplayer.Components
public bool IsEmpty => string.IsNullOrEmpty(Text);
public PlayerNameLineEdit()
public override void _Ready()
{
base._Ready();
TextChanged += OnTextChanged;
}
@@ -32,7 +32,8 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
{
var netService = new NetHostGameService();
NetErrorInfo? netErrorInfo = null;
netService.StartENetHost(port, maxPlayers);
//Add one more max client to send the full lobby message
netService.StartENetHost(port, maxPlayers + 1);
Log.Info($"HostGame open on port:{port}");
if (!netErrorInfo.HasValue)
{
@@ -94,7 +95,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
{
var netService = new NetHostGameService();
NetErrorInfo? netErrorInfo = null;
netService.StartENetHost(port, maxPlayers);
netService.StartENetHost(port, maxPlayers + 1);
Log.Info($"HostGame open on port:{port}");
if (!netErrorInfo.HasValue)
{
@@ -0,0 +1,114 @@
using System.Reflection;
using HarmonyLib;
using MegaCrit.Sts2.Core.Multiplayer.Serialization;
using SlayTheSpire2.LAN.Multiplayer.Patchs;
// ReSharper disable ClassNeverInstantiated.Global
namespace SlayTheSpire2.LAN.Multiplayer.Helpers
{
internal class PacketHelper
{
private static readonly Action<PacketWriter, int> SetPacketWriterBitPosition;
private static readonly Action<PacketReader, int> SetPacketReaderBitPosition;
private static readonly AccessTools.FieldRef<PacketWriter, byte[]> RefPacketWriterTempBuffer;
private static readonly AccessTools.FieldRef<PacketReader, byte[]> RefPacketReaderTempBuffer;
static PacketHelper()
{
const BindingFlags flags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public;
SetPacketWriterBitPosition =
AccessTools.MethodDelegate<Action<PacketWriter, int>>(typeof(PacketWriter)
.GetProperty("BitPosition", flags)
?.SetMethod!);
SetPacketReaderBitPosition =
AccessTools.MethodDelegate<Action<PacketReader, int>>(typeof(PacketReader)
.GetProperty("BitPosition", flags)
?.SetMethod!);
RefPacketWriterTempBuffer =
AccessTools.FieldRefAccess<PacketWriter, byte[]>("_tempBuffer");
RefPacketReaderTempBuffer =
AccessTools.FieldRefAccess<PacketReader, byte[]>("_tempBuffer");
}
public static void WriteList<T>(PacketWriter instance, IReadOnlyList<T> list)
where T : IPacketSerializable, new()
{
WriteVarInt(instance, (uint)list.Count);
foreach (var item in list)
{
instance.Write(item);
}
}
public static void WriteVarInt(PacketWriter writer, uint val)
{
var tempBuffer = RefPacketWriterTempBuffer(writer);
var span = tempBuffer.AsSpan();
var bytesWritten = 0;
while (val >= 0x80)
{
span[bytesWritten++] = (byte)((val & 0x7F) | 0x80);
val >>= 7;
}
span[bytesWritten++] = (byte)val;
var totalBits = bytesWritten * 8;
BitSerializationUtilWriteBytesPatch.WriteBytes(tempBuffer, writer.Buffer, writer.BitPosition, totalBits);
SetPacketWriterBitPosition(writer, writer.BitPosition + totalBits);
}
public static List<T> ReadList<T>(PacketReader reader) where T : IPacketSerializable, new()
{
var list = new List<T>();
var num = ReadVarInt(reader);
for (var i = 0; i < num; i++)
{
list.Add(reader.Read<T>());
}
return list;
}
public static uint ReadVarInt(PacketReader reader)
{
var tempBuffer = RefPacketReaderTempBuffer(reader);
uint result = 0;
var shift = 0;
while (true)
{
Array.Clear(tempBuffer);
//7-bit VarInt and 1-bit Flag
BitSerializationUtilReadBitsPatch.ReadBits(reader.Buffer, reader.BitPosition, tempBuffer, 8);
SetPacketReaderBitPosition(reader, reader.BitPosition + 8);
result |= (uint)(tempBuffer[0] & 0x7F) << shift;
if ((tempBuffer[0] & 0x80) == 0)
break;
shift += 7;
if (shift >= 35)
{
throw new Exception("VarInt Invalid");
}
}
return result;
}
}
}
@@ -0,0 +1,43 @@
using System.Reflection;
using HarmonyLib;
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
[HarmonyPatch]
internal class BitSerializationUtilWriteBytesPatch
{
private static MethodInfo TargetMethod()
{
return AccessTools.TypeByName("MegaCrit.Sts2.Core.Multiplayer.Serialization.BitSerializationUtil")
.GetMethod("WriteBytes", BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public)!;
}
[HarmonyReversePatch]
public static void WriteBytes(byte[] originBuffer, byte[] destinationBuffer, int destinationBitPosition,
int totalBitsToWrite)
{
throw new NotImplementedException();
}
}
[HarmonyPatch]
internal class BitSerializationUtilReadBitsPatch
{
private static MethodInfo TargetMethod()
{
return AccessTools.TypeByName("MegaCrit.Sts2.Core.Multiplayer.Serialization.BitSerializationUtil")
.GetMethod("ReadBits", BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public)!;
}
[HarmonyReversePatch]
public static void ReadBits(byte[] originBuffer, int originBitPosition, byte[] destinationBuffer,
int totalBitsToRead)
{
throw new NotImplementedException();
}
}
}
@@ -15,6 +15,13 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.ENet
[HarmonyPatch(typeof(ENetClient), "ConnectToHost")]
internal class ENetClientConnectToHostPatch
{
[HarmonyReversePatch]
[HarmonyPatch(typeof(ENetClient), "HandleMessageReceived")]
private static void HandleMessageReceived(ENetClient instance, ENetServiceData data)
{
throw new NotImplementedException();
}
private static bool Prefix(ENetClient __instance, ulong netId, string ip, ushort port,
CancellationToken cancelToken, Logger ____logger, INetClientHandler ____handler, ref Task __result)
{
@@ -82,7 +89,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.ENet
handler.OnConnectedToHost();
foreach (var item in bufferedPackets)
{
Traverse.Create(eNetClient).Method("HandleMessageReceived", item).GetValue();
HandleMessageReceived(eNetClient, item);
}
return null;
@@ -3,6 +3,7 @@ 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 MegaCrit.Sts2.Core.Platform;
using SlayTheSpire2.LAN.Multiplayer.Models;
using SlayTheSpire2.LAN.Multiplayer.Services;
@@ -24,7 +25,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
var result = await clientLobbyJoinResponseMessage;
if (joinFlow.NetService == null)
if (joinFlow.NetService is not { Platform: PlatformType.None })
return result;
var lanPlayerNameService = LanPlayerNameService.Instance;
@@ -51,7 +52,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
var result = await clientLoadJoinResponseMessage;
if (joinFlow.NetService == null)
if (joinFlow.NetService is not { Platform: PlatformType.None })
return result;
var lanPlayerNameService = LanPlayerNameService.Instance;
@@ -78,7 +79,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
var result = await clientRejoinResponseMessage;
if (joinFlow.NetService == null)
if (joinFlow.NetService is not { Platform: PlatformType.None })
return result;
var lanPlayerNameService = LanPlayerNameService.Instance;
@@ -95,25 +96,30 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
[HarmonyPatch(typeof(JoinFlow), "OnDisconnected")]
internal class JoinFlowOnDisconnectedPatch
{
private static void Postfix(NetErrorInfo info)
private static void Postfix(JoinFlow __instance, NetErrorInfo info)
{
if (__instance.NetService is { Platform: PlatformType.None })
{
var lanPlayerNameCompletion = LanPlayerNameService.Instance.LanPlayerNameCompletion;
if (lanPlayerNameCompletion?.Task is { IsCompleted: false })
{
var exception =
new ClientConnectionFailedException(
$"Unexpectedly disconnected from host while joining. Reason: {info.GetReason()}", info);
var lanPlayerNameCompletion = LanPlayerNameService.Instance.LanPlayerNameCompletion;
if (lanPlayerNameCompletion?.Task is { IsCompleted: false })
{
lanPlayerNameCompletion.SetException(exception);
}
}
}
}
[HarmonyPatch(typeof(JoinFlow), "Cancel")]
internal class JoinFlowCancelPatch
{
private static void Postfix()
private static void Postfix(JoinFlow __instance)
{
if (__instance.NetService is { Platform: PlatformType.None })
{
var lanPlayerNameCompletion = LanPlayerNameService.Instance.LanPlayerNameCompletion;
@@ -123,4 +129,5 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
}
}
}
}
}
@@ -0,0 +1,65 @@
using HarmonyLib;
using MegaCrit.Sts2.Core.Daily;
using MegaCrit.Sts2.Core.Entities.Multiplayer;
using MegaCrit.Sts2.Core.Multiplayer.Messages.Lobby;
using MegaCrit.Sts2.Core.Multiplayer.Serialization;
using MegaCrit.Sts2.Core.Saves.Runs;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Messages
{
[HarmonyPatch(typeof(ClientLobbyJoinResponseMessage), "Serialize")]
internal class ClientLobbyJoinResponseMessageSerializePatch
{
private static bool Prefix(ClientLobbyJoinResponseMessage __instance, PacketWriter writer)
{
if (__instance.playersInLobby == null)
{
throw new InvalidOperationException("Tried to serialize ClientSlotGrantedMessage with null list!");
}
PacketHelper.WriteList(writer, __instance.playersInLobby);
writer.WriteBool(__instance.dailyTime.HasValue);
if (__instance.dailyTime.HasValue)
{
writer.Write(__instance.dailyTime.Value);
}
writer.WriteBool(__instance.seed != null);
if (__instance.seed != null)
{
writer.WriteString(__instance.seed);
}
writer.WriteInt(__instance.ascension, 5);
writer.WriteList(__instance.modifiers);
return false;
}
}
[HarmonyPatch(typeof(ClientLobbyJoinResponseMessage), "Deserialize")]
internal class ClientLobbyJoinResponseMessageDeserializePatch
{
private static bool Prefix(ref ClientLobbyJoinResponseMessage __instance, PacketReader reader)
{
__instance.playersInLobby = PacketHelper.ReadList<LobbyPlayer>(reader);
if (reader.ReadBool())
{
__instance.dailyTime = reader.Read<TimeServerResult>();
}
if (reader.ReadBool())
{
__instance.seed = reader.ReadString();
}
__instance.ascension = reader.ReadInt(5);
__instance.modifiers = reader.ReadList<SerializableModifier>();
return false;
}
}
}
@@ -0,0 +1,45 @@
using HarmonyLib;
using MegaCrit.Sts2.Core.Entities.Multiplayer;
using MegaCrit.Sts2.Core.Multiplayer.Messages.Lobby;
using MegaCrit.Sts2.Core.Multiplayer.Serialization;
using MegaCrit.Sts2.Core.Saves.Runs;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Messages
{
[HarmonyPatch(typeof(LobbyBeginRunMessage), "Serialize")]
internal class LobbyBeginRunMessageSerializePatch
{
private static bool Prefix(LobbyBeginRunMessage __instance, PacketWriter writer)
{
if (__instance.playersInLobby == null)
{
throw new InvalidOperationException("Tried to serialize ClientSlotGrantedMessage with null list!");
}
PacketHelper.WriteList(writer, __instance.playersInLobby);
writer.WriteString(__instance.seed);
writer.WriteList(__instance.modifiers);
writer.WriteString(__instance.act1);
return false;
}
}
[HarmonyPatch(typeof(LobbyBeginRunMessage), "Deserialize")]
internal class LobbyBeginRunMessageDeserializePatch
{
private static bool Prefix(ref LobbyBeginRunMessage __instance, PacketReader reader)
{
__instance.playersInLobby = PacketHelper.ReadList<LobbyPlayer>(reader);
__instance.seed = reader.ReadString();
__instance.modifiers = reader.ReadList<SerializableModifier>();
__instance.act1 = reader.ReadString();
return false;
}
}
}
@@ -0,0 +1,44 @@
using HarmonyLib;
using MegaCrit.Sts2.Core.Entities.Multiplayer;
using MegaCrit.Sts2.Core.Models;
using MegaCrit.Sts2.Core.Multiplayer.Serialization;
using MegaCrit.Sts2.Core.Unlocks;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Messages
{
[HarmonyPatch(typeof(LobbyPlayer), "Serialize")]
internal class LobbyPlayerSerializePatch
{
private static bool Prefix(LobbyPlayer __instance, PacketWriter writer)
{
writer.WriteULong(__instance.id);
PacketHelper.WriteVarInt(writer, (uint)__instance.slotId);
writer.WriteModel(__instance.character);
writer.Write(__instance.unlockState);
writer.WriteInt(__instance.maxMultiplayerAscensionUnlocked);
writer.WriteBool(__instance.isReady);
return false;
}
}
[HarmonyPatch(typeof(LobbyPlayer), "Deserialize")]
internal class LobbyPlayerDeserializePatch
{
private static bool Prefix(ref LobbyPlayer __instance, PacketReader reader)
{
__instance.id = reader.ReadULong();
__instance.slotId = (int)PacketHelper.ReadVarInt(reader);
__instance.character = reader.ReadModel<CharacterModel>();
__instance.unlockState = reader.Read<SerializableUnlockState>();
__instance.maxMultiplayerAscensionUnlocked = reader.ReadInt();
__instance.isReady = reader.ReadBool();
return false;
}
}
}
@@ -2,7 +2,6 @@
using HarmonyLib;
using MegaCrit.Sts2.addons.mega_text;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Localization;
using MegaCrit.Sts2.Core.Multiplayer.Connection;
using MegaCrit.Sts2.Core.Nodes.GodotExtensions;
using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu;
@@ -20,7 +19,6 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
private static void Prefix(NJoinFriendScreen __instance)
{
var lanPanel = new NinePatchRect { Name = "LANPanel" };
__instance.AddChild(lanPanel);
lanPanel.PatchMarginTop = 12;
@@ -66,16 +64,12 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
addressLineEdit.CustomMinimumSize = new Vector2(300, 50);
addressLineEdit.SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter;
if (__instance.GetNode<NJoinFriendRefreshButton>("RefreshButton").Duplicate() is NJoinFriendRefreshButton
joinButton)
{
var joinButton = JoinButton.Create(__instance.GetNode<NJoinFriendRefreshButton>("RefreshButton"));
joinButton.Name = "JointButton";
vBoxContainer.AddChild(joinButton);
joinButton.CustomMinimumSize = new Vector2(150, 50);
joinButton.SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter;
joinButton.Connect(NClickableControl.SignalName.Released, Callable.From<NClickableControl>(_ =>
{
var addressInfo = addressLineEdit.GetAddressInfo();
@@ -101,14 +95,6 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
SettingsService.Instance.SettingsModel.NetId, addressInfo.Address, port)));
}
}));
joinButton.Material = joinButton.Material.Duplicate() as Material;
Traverse.Create(joinButton).Field("_hsv").SetValue(joinButton.Material);
var joinButtonLabel = joinButton.GetNode<MegaLabel>("Label");
joinButtonLabel.SetTextAutoSize(new LocString("main_menu_ui", "JOIN.title").GetFormattedText());
}
}
}
}
@@ -1,4 +1,5 @@
using HarmonyLib;
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu;
using SlayTheSpire2.LAN.Multiplayer.Components;
@@ -15,7 +16,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{
if (type == typeof(LanMultiplayerHostSubmenu))
{
if (LanMultiplayerHostSubmenu.Instance == null)
if (!GodotObject.IsInstanceValid(LanMultiplayerHostSubmenu.Instance))
{
var lanMultiplayerHostSubmenu = LanMultiplayerHostSubmenu.Create();
@@ -17,6 +17,13 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
[HarmonyPatch(typeof(NMultiplayerPlayerExpandedState), "_Ready")]
internal class NMultiplayerPlayerExpandedStateReadyPatch
{
[HarmonyReversePatch]
[HarmonyPatch(typeof(NMapDrawings), "GetDrawingStateForPlayer")]
private static object GetDrawingStateForPlayer(NMapDrawings instance, ulong playerId)
{
throw new NotImplementedException();
}
private static void Prefix(NMultiplayerPlayerExpandedState __instance, Player ____player)
{
if (____player.NetId != RunManager.Instance.NetService.NetId)
@@ -48,8 +55,11 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
cardLibraryTickBox.Toggled += tickBox =>
{
var drawingState = Traverse.Create(NMapScreen.Instance?.Drawings)
.Method("GetDrawingStateForPlayer", ____player.NetId).GetValue();
if (NMapScreen.Instance == null)
return;
var drawingState = GetDrawingStateForPlayer(NMapScreen.Instance.Drawings, ____player.NetId);
var drawViewport = Traverse.Create(drawingState).Field("drawViewport").GetValue<SubViewport>();
if (tickBox.IsTicked)
@@ -18,12 +18,20 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
[HarmonyPatch(typeof(NMultiplayerSubmenu), "_Ready")]
internal class NMultiplayerSubmenuReadyPatch
{
[HarmonyReversePatch(HarmonyReversePatchType.Snapshot)]
[HarmonyPatch(typeof(NMultiplayerSubmenu), "UpdateButtons")]
private static void UpdateButtons(NMultiplayerSubmenu instance)
{
throw new NotImplementedException();
}
private static void Prefix(NMultiplayerSubmenu __instance)
{
var buttonContainerNode = __instance.GetNode("ButtonContainer");
if (buttonContainerNode.GetNode("HostButton").Duplicate() is not NSubmenuButton lanHostButton)
return;
if (buttonContainerNode.GetNode("HostButton").Duplicate() is NSubmenuButton lanHostButton)
{
NSubmenuButtonDuplicateMaterial(lanHostButton);
buttonContainerNode.AddChild(lanHostButton);
buttonContainerNode.MoveChild(lanHostButton, 1);
@@ -52,14 +60,12 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
var lanHostTitle = Traverse.Create(lanHostButton).Field("_title").GetValue<MegaLabel>();
lanHostTitle.Text = $"LAN {lanHostTitle.Text}";
NSubmenuButtonDuplicateMaterial(lanHostButton);
LanMultiplayerSubmenuButtonService.Instance.LanHostButton = lanHostButton;
}
var lanMultiplayerSubmenuButtonService = LanMultiplayerSubmenuButtonService.Instance;
lanMultiplayerSubmenuButtonService.LanHostButton = lanHostButton;
if (buttonContainerNode.GetNode("LoadButton").Duplicate() is not NSubmenuButton lanLoadButton)
return;
if (buttonContainerNode.GetNode("LoadButton").Duplicate() is NSubmenuButton lanLoadButton)
{
NSubmenuButtonDuplicateMaterial(lanLoadButton);
buttonContainerNode.AddChild(lanLoadButton);
buttonContainerNode.MoveChild(lanLoadButton, 2);
@@ -79,12 +85,12 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
var lanLoadButtonTitle = Traverse.Create(lanLoadButton).Field("_title").GetValue<MegaLabel>();
lanLoadButtonTitle.Text = $"LAN {lanLoadButtonTitle.Text}";
NSubmenuButtonDuplicateMaterial(lanLoadButton);
LanMultiplayerSubmenuButtonService.Instance.LanLoadButton = lanLoadButton;
}
lanMultiplayerSubmenuButtonService.LanLoadButton = lanLoadButton;
if (buttonContainerNode.GetNode("AbandonButton").Duplicate() is not NSubmenuButton lanAbandonButton)
return;
if (buttonContainerNode.GetNode("AbandonButton").Duplicate() is NSubmenuButton lanAbandonButton)
{
NSubmenuButtonDuplicateMaterial(lanAbandonButton);
buttonContainerNode.AddChild(lanAbandonButton);
buttonContainerNode.MoveChild(lanAbandonButton, 3);
@@ -92,28 +98,21 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
lanAbandonButton.Connect(NClickableControl.SignalName.Released,
Callable.From<NButton>(_ =>
{
var traverse = Traverse.Create(__instance);
TaskHelper.RunSafely(
LanHostHelper.TryAbandonMultiplayerRun(() => traverse.Method("UpdateButtons").GetValue()));
LanHostHelper.TryAbandonMultiplayerRun(() => UpdateButtons(__instance)));
}));
lanAbandonButton.SetIconAndLocalization("MP_ABANDON");
var lanAbandonButtonTitle = Traverse.Create(lanAbandonButton).Field("_title").GetValue<MegaLabel>();
lanAbandonButtonTitle.Text = $"LAN {lanAbandonButtonTitle.Text}";
NSubmenuButtonDuplicateMaterial(lanAbandonButton);
lanMultiplayerSubmenuButtonService.LanAbandonButton = lanAbandonButton;
LanMultiplayerSubmenuButtonService.Instance.LanAbandonButton = lanAbandonButton;
}
}
private static void NSubmenuButtonDuplicateMaterial(NSubmenuButton nSubmenuButton)
{
var traverse = Traverse.Create(nSubmenuButton);
var bgPanel = traverse.Field("_bgPanel").GetValue<Control>();
var bgPanel = nSubmenuButton.GetNode<Control>("BgPanel");
bgPanel.Material = bgPanel.Material.Duplicate() as Material;
traverse.Field("_hsv").SetValue(bgPanel.Material);
}
}
@@ -1,5 +1,7 @@
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.addons.mega_text;
using MegaCrit.Sts2.Core.Localization;
using MegaCrit.Sts2.Core.Nodes.Screens.Settings;
using SlayTheSpire2.LAN.Multiplayer.Components;
using SlayTheSpire2.LAN.Multiplayer.Services;
@@ -12,6 +14,13 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
[HarmonyPatch(typeof(NSettingsScreen), "_Ready")]
internal class NSettingsScreenReadyPatch
{
[HarmonyReversePatch]
[HarmonyPatch(typeof(NSettingsPanel), "RefreshSize")]
private static void RefreshSize(NSettingsPanel instance)
{
throw new NotImplementedException();
}
private static void Prefix(NSettingsScreen __instance)
{
var moddingNode = __instance.GetNode("%Modding");
@@ -21,7 +30,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
if (__instance.GetNode("%ModdingDivider").Duplicate() is ColorRect hostPortDivider &&
moddingNode.Duplicate() is MarginContainer hostPort &&
hostPort.GetNode("Label") is RichTextLabel hostPortLabel)
hostPort.GetNode("Label") is MegaRichTextLabel hostPortLabel)
{
hostPortDivider.Name = "HostPortDivider";
@@ -40,7 +49,6 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
hostPort.Show();
var hostPortLineEdit = new SpinBox { Name = "HostPortInput" };
hostPort.AddChild(hostPortLineEdit);
hostPortLineEdit.CustomMinimumSize = new Vector2(324, 64);
@@ -62,7 +70,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
if (__instance.GetNode("%ModdingDivider").Duplicate() is ColorRect hostMaxPlayersDivider &&
moddingNode.Duplicate() is MarginContainer hostMaxPlayers &&
hostMaxPlayers.GetNode("Label") is RichTextLabel hostMaxPlayersLabel)
hostMaxPlayers.GetNode("Label") is MegaRichTextLabel hostMaxPlayersLabel)
{
hostMaxPlayersDivider.Name = "HostMaxPlayersDivider";
@@ -81,7 +89,6 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
hostMaxPlayers.Show();
var hostMaxPlayersInput = new SpinBox { Name = "HostMaxPlayersInput" };
hostMaxPlayers.AddChild(hostMaxPlayersInput);
hostMaxPlayersInput.CustomMinimumSize = new Vector2(324, 64);
@@ -102,7 +109,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
if (__instance.GetNode("%ModdingDivider").Duplicate() is ColorRect playerNameDivider &&
moddingNode.Duplicate() is MarginContainer playerName &&
playerName.GetNode("Label") is RichTextLabel playerNameLabel)
playerName.GetNode("Label") is MegaRichTextLabel playerNameLabel)
{
playerNameDivider.Name = "PlayerNameDivider";
@@ -120,11 +127,15 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
playerName.Show();
var marginContainer = new MarginContainer();
playerName.AddChild(marginContainer);
marginContainer.AddThemeConstantOverride("margin_right", 18);
var playerNameInput = new PlayerNameLineEdit { Name = "PlayerNameInput" };
marginContainer.AddChild(playerNameInput);
playerName.AddChild(playerNameInput);
playerNameInput.CustomMinimumSize = new Vector2(324, 64);
playerNameInput.CustomMinimumSize = new Vector2(308, 64);
playerNameInput.SizeFlagsHorizontal = Control.SizeFlags.ShrinkEnd;
playerNameInput.Alignment = HorizontalAlignment.Center;
@@ -145,7 +156,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
if (__instance.GetNode("%ModdingDivider").Duplicate() is ColorRect netIdDivider &&
moddingNode.Duplicate() is MarginContainer netId &&
netId.GetNode("Label") is RichTextLabel netIdLabel)
netId.GetNode("Label") is MegaRichTextLabel netIdLabel)
{
netIdDivider.Name = "NetIDDivider";
@@ -164,7 +175,6 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
netId.Show();
var netIdInput = new SpinBox { Name = "NetIDInput" };
netId.AddChild(netIdInput);
netIdInput.CustomMinimumSize = new Vector2(324, 64);
@@ -186,8 +196,33 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
if (generalSettings is NSettingsPanel nSettingsPanel)
{
Traverse.Create(nSettingsPanel).Method("RefreshSize").GetValue();
RefreshSize(nSettingsPanel);
}
}
}
[HarmonyPatch(typeof(NSettingsScreen), "LocalizeLabels")]
internal class NSettingsScreenLocalizeLabelsPatch
{
[HarmonyReversePatch]
[HarmonyPatch(typeof(NSettingsScreen), "LocHelper")]
private static void LocHelper(Node settingsLineNode, LocString locString)
{
throw new NotImplementedException();
}
private static void Prefix(NSettingsScreen __instance)
{
var content = __instance.GetNode<NSettingsPanel>("%GeneralSettings").Content;
LocHelper(content.GetNode<Node>("HostPort"),
new LocString("settings_ui", "SlayTheSpire2.LAN.Multiplayer.HOST_PORT"));
LocHelper(content.GetNode<Node>("HostMaxPlayers"),
new LocString("settings_ui", "SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS"));
LocHelper(content.GetNode<Node>("PlayerName"),
new LocString("settings_ui", "SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME"));
LocHelper(content.GetNode<Node>("NetID"),
new LocString("settings_ui", "SlayTheSpire2.LAN.Multiplayer.NET_ID"));
}
}
}
@@ -5,6 +5,7 @@ using MegaCrit.Sts2.Core.Multiplayer.Game.Lobby;
using MegaCrit.Sts2.Core.Nodes.Screens.CharacterSelect;
using MegaCrit.Sts2.Core.Nodes.Screens.CustomRun;
using MegaCrit.Sts2.Core.Nodes.Screens.DailyRun;
using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu;
using MegaCrit.Sts2.Core.Platform;
using SlayTheSpire2.LAN.Multiplayer.Services;
@@ -14,21 +15,52 @@ using SlayTheSpire2.LAN.Multiplayer.Services;
namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{
[HarmonyPatch]
internal class RunLoadScreenInitializePatchs
internal class RunLoadScreenInitializeAsHostPatchs
{
private static IEnumerable<MethodInfo> TargetMethods()
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
yield return typeof(NMultiplayerLoadGameScreen).GetMethod("InitializeAsHost", flags)!;
yield return typeof(NMultiplayerLoadGameScreen).GetMethod("InitializeAsClient", flags)!;
yield return typeof(NDailyRunLoadScreen).GetMethod("InitializeAsHost", flags)!;
yield return typeof(NDailyRunLoadScreen).GetMethod("InitializeAsClient", flags)!;
yield return typeof(NCustomRunLoadScreen).GetMethod("InitializeAsHost", flags)!;
}
private static void Prefix(NSubmenu __instance, INetGameService gameService)
{
if (gameService.Platform == PlatformType.None)
{
var runScreenService = RunScreenService.Instance;
switch (__instance)
{
case NMultiplayerLoadGameScreen multiplayerLoadGameScreen:
runScreenService.MultiplayerLoadGameScreen = multiplayerLoadGameScreen;
break;
case NDailyRunLoadScreen dailyRunLoadScreen:
runScreenService.DailyRunLoadScreen = dailyRunLoadScreen;
break;
case NCustomRunLoadScreen customRunLoadScreen:
runScreenService.CustomRunLoadScreen = customRunLoadScreen;
break;
}
}
}
}
[HarmonyPatch]
internal class RunLoadScreenInitializeAsClientPatchs
{
private static IEnumerable<MethodInfo> TargetMethods()
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
yield return typeof(NMultiplayerLoadGameScreen).GetMethod("InitializeAsClient", flags)!;
yield return typeof(NDailyRunLoadScreen).GetMethod("InitializeAsClient", flags)!;
yield return typeof(NCustomRunLoadScreen).GetMethod("InitializeAsClient", flags)!;
}
private static void Prefix(object __instance, INetGameService gameService)
private static void Prefix(NSubmenu __instance, INetGameService gameService)
{
if (gameService.Platform == PlatformType.None)
{
@@ -4,7 +4,9 @@ using MegaCrit.Sts2.Core.Multiplayer.Game;
using MegaCrit.Sts2.Core.Nodes.Screens.CharacterSelect;
using MegaCrit.Sts2.Core.Nodes.Screens.CustomRun;
using MegaCrit.Sts2.Core.Nodes.Screens.DailyRun;
using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu;
using MegaCrit.Sts2.Core.Platform;
using SlayTheSpire2.LAN.Multiplayer.Components;
using SlayTheSpire2.LAN.Multiplayer.Services;
// ReSharper disable UnusedMember.Global
@@ -13,21 +15,41 @@ using SlayTheSpire2.LAN.Multiplayer.Services;
namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{
[HarmonyPatch]
internal class RunScreenInitializePatchs
internal class RunScreenReadyPatchs
{
private static IEnumerable<MethodInfo> TargetMethods()
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
yield return typeof(NCharacterSelectScreen).GetMethod("_Ready", flags)!;
yield return typeof(NDailyRunScreen).GetMethod("_Ready", flags)!;
yield return typeof(NCustomRunScreen).GetMethod("_Ready", flags)!;
}
private static void Prefix(NSubmenu __instance)
{
var ipAddressInfoPanel = IPAddressInfoPanel.Create();
ipAddressInfoPanel.Name = "IPAddressPanel";
__instance.AddChild(ipAddressInfoPanel);
ipAddressInfoPanel.Visible = false;
}
}
[HarmonyPatch]
internal class RunScreenInitializeAsHostPatchs
{
private static IEnumerable<MethodInfo> TargetMethods()
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
yield return typeof(NCharacterSelectScreen).GetMethod("InitializeMultiplayerAsClient", flags)!;
yield return typeof(NCharacterSelectScreen).GetMethod("InitializeMultiplayerAsHost", flags)!;
yield return typeof(NDailyRunScreen).GetMethod("InitializeMultiplayerAsClient", flags)!;
yield return typeof(NDailyRunScreen).GetMethod("InitializeMultiplayerAsHost", flags)!;
yield return typeof(NCustomRunScreen).GetMethod("InitializeMultiplayerAsClient", flags)!;
yield return typeof(NCustomRunScreen).GetMethod("InitializeMultiplayerAsHost", flags)!;
}
private static void Prefix(object __instance, INetGameService gameService)
private static void Prefix(NSubmenu __instance, INetGameService gameService)
{
if (gameService.Platform == PlatformType.None)
{
@@ -47,5 +69,84 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
}
}
}
private static void Postfix(NSubmenu __instance, INetGameService gameService)
{
if (__instance.GetNode("IPAddressPanel") is IPAddressInfoPanel ipAddressInfoPanel)
{
if (gameService.Platform == PlatformType.None)
{
ipAddressInfoPanel.Initialize();
ipAddressInfoPanel.Visible = true;
}
else
{
ipAddressInfoPanel.Visible = false;
}
}
}
}
[HarmonyPatch]
internal class RunScreenInitializeAsClientPatchs
{
private static IEnumerable<MethodInfo> TargetMethods()
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
yield return typeof(NCharacterSelectScreen).GetMethod("InitializeMultiplayerAsClient", flags)!;
yield return typeof(NDailyRunScreen).GetMethod("InitializeMultiplayerAsClient", flags)!;
yield return typeof(NCustomRunScreen).GetMethod("InitializeMultiplayerAsClient", flags)!;
}
private static void Prefix(NSubmenu __instance, INetGameService gameService)
{
if (gameService.Platform == PlatformType.None)
{
var runScreenService = RunScreenService.Instance;
switch (__instance)
{
case NCharacterSelectScreen characterSelectScreen:
runScreenService.CharacterSelectScreen = characterSelectScreen;
break;
case NDailyRunScreen dailyRunScreen:
runScreenService.DailyRunScreen = dailyRunScreen;
break;
case NCustomRunScreen customRunScreen:
runScreenService.CustomRunScreen = customRunScreen;
break;
}
}
}
private static void Postfix(NSubmenu __instance)
{
if (__instance.GetNode("IPAddressPanel") is IPAddressInfoPanel ipAddressInfoPanel)
{
ipAddressInfoPanel.Visible = false;
}
}
}
[HarmonyPatch]
internal class RunScreenInitializeSingleplayerPatchs
{
private static IEnumerable<MethodInfo> TargetMethods()
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
yield return typeof(NCharacterSelectScreen).GetMethod("InitializeSingleplayer", flags)!;
yield return typeof(NDailyRunScreen).GetMethod("InitializeSingleplayer", flags)!;
yield return typeof(NCustomRunScreen).GetMethod("InitializeSingleplayer", flags)!;
}
private static void Postfix(NSubmenu __instance)
{
if (__instance.GetNode("IPAddressPanel") is IPAddressInfoPanel ipAddressInfoPanel)
{
ipAddressInfoPanel.Visible = false;
}
}
}
}
@@ -55,7 +55,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Services
nVerticalPopup.YesButton.Visible = false;
return await nGenericPopup.WaitForConfirmation(locString,
await nGenericPopup.WaitForConfirmation(locString,
new LocString("gameplay_ui", "CONFIRM_LOAD_SAVE.header"),
new LocString("gameplay_ui", "CONFIRM_LOAD_SAVE.cancel"),
new LocString("gameplay_ui", "CONFIRM_LOAD_SAVE.confirm"));
@@ -8,6 +8,10 @@
<FileVersion>1.5.0.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Godot.SourceGenerators" Version="4.5.1" />
</ItemGroup>
<ItemGroup>
<Reference Include="0Harmony">
<HintPath>V:\Slay.the.Spire.2\data_sts2_windows_x86_64\0Harmony.dll</HintPath>
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Kopiert",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "IP Adresse:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Lokale IP Adresse:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "IPV6 Adresse:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Host Port",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Max Spieleranzahl",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Spielername",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Copied",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "IP Address:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Local IP Address:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "IPV6 Address:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Host Port",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Host Max Players",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Player Name",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Copiado",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "Dirección IP:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Dirección IP local:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "Dirección IPV6:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Puerto del host",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Máximo de jugadores",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Nombre del jugador",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Copié",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "Adresse IP:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Adresse IP locale:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "Adresse IPV6:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Port de l'hôte",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Nombre max de joueurs",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Nom du joueur",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Copiato",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "Indirizzo IP:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Indirizzo IP locale:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "Indirizzo IPV6:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Porta Host",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Numero massimo giocatori",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Nome giocatore",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "コピーしました",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "IPアドレス:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "ローカルIPアドレス:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "IPV6アドレス:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "ホストポート",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "最大プレイヤー数",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "プレイヤー名",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "복사됨",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "IP주소:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "로컬IP주소:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "IPV6주소:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "호스트 포트",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "호스트 최대 인원",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "플레이어 이름",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Skopiowano",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "Adres IP:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Lokalny adres IP:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "Adres IPV6:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Port hosta",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Maks. liczba graczy",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Nazwa gracza",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Copiado",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "Endereço IP:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Endereço IP Local:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "Endereço IPV6:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Porta do Host",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Máximo de Jogadores",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Nome do Jogador",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Скопировано",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "IP адрес:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Локальный IP адрес:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "IPV6 адрес:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Порт хоста",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Макс. игроков",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Имя игрока",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Copiado",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "Dirección IP:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Dirección IP local:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "Dirección IPV6:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Puerto del anfitrión",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Máximo de jugadores",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Nombre del jugador",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "คัดลอกแล้ว",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "ที่อยู่ IP:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "ที่อยู่ IP ภายใน:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "ที่อยู่ IPV6:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "พอร์ตโฮสต์",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "จำนวนผู้เล่นสูงสุด",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "ชื่อผู้เล่น",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "Kopyalandı",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "IP Adresi:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "Yerel IP Adresi:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "IPv6 Adresi:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "Sunucu Portu",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "Maksimum Oyuncu",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "Oyuncu Adı",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.COPIED": "已复制",
"SlayTheSpire2.LAN.Multiplayer.IP_ADDRESS_TITLE": "IP地址:",
"SlayTheSpire2.LAN.Multiplayer.LOCAL_IP_ADDRESS_TITLE": "本地IP地址:",
"SlayTheSpire2.LAN.Multiplayer.IPV6_ADDRESS_TITLE": "IPV6地址:"
}
+6
View File
@@ -0,0 +1,6 @@
{
"SlayTheSpire2.LAN.Multiplayer.HOST_PORT": "主机端口",
"SlayTheSpire2.LAN.Multiplayer.HOST_MAX_PLAYERS": "主机最大玩家数",
"SlayTheSpire2.LAN.Multiplayer.PLAYER_NAME": "玩家名称",
"SlayTheSpire2.LAN.Multiplayer.NET_ID": "NetID"
}