Compare commits

...
10 Commits
Author SHA1 Message Date
kmyuhkyuk c0d9e66fc8 Added DISABLE_DRAWING Localization Entry 2026-03-30 23:49:53 +08:00
kmyuhkyuk 79876ed68b Update version 2026-03-28 23:16:22 +08:00
kmyuhkyuk 047318373a Fixed PlayerExpandedState display error 2026-03-28 20:41:42 +08:00
kmyuhkyuk 32b55c42be Update version 2026-03-27 23:32:31 +08:00
kmyuhkyuk a2b2351398 Merge pull request #12 from kmyuhkyuk/FixCombatStateSynchronizer
Merge FixCombatStateSynchronizer branch
2026-03-27 23:21:36 +08:00
kmyuhkyuk 301b0aecf3 Fixed unit mistake 2026-03-25 23:15:39 +08:00
kmyuhkyuk 1bf1e32b3d Now LanPlayerNameResponseMessage used 7-bit variant to serialize and deserialize 2026-03-25 19:17:53 +08:00
kmyuhkyuk 309087a8f4 Fixed LAN Save will not be deleted after the game end
Fixed LAN Buttons visible cannot be updated normally after abandon
2026-03-25 18:13:20 +08:00
kmyuhkyuk 2fbfa5a6d6 Fixed ENetClient ConnectToHost can't return NetErrorInfo
Added CombatStateSynchronizer Timeout and Resend mechanism
2026-03-25 16:27:21 +08:00
kmyuhkyuk ddb56fd903 Format merge pull request 2026-03-24 09:24:47 +08:00
26 changed files with 387 additions and 35 deletions
@@ -1,4 +1,6 @@
using Godot;
using System.Reflection;
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.Core.Daily;
using MegaCrit.Sts2.Core.Entities.Multiplayer;
using MegaCrit.Sts2.Core.Helpers;
@@ -24,6 +26,15 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
{
internal class LanHostHelper
{
private static readonly Action<NMultiplayerSubmenu> UpdateButtons;
static LanHostHelper()
{
UpdateButtons = AccessTools.MethodDelegate<Action<NMultiplayerSubmenu>>(
typeof(NMultiplayerSubmenu).GetMethod("UpdateButtons",
BindingFlags.Instance | BindingFlags.NonPublic)!);
}
public static void StartHost(GameMode gameMode, Control loadingOverlay, NSubmenuStack stack, ushort port,
int maxPlayers)
{
@@ -163,7 +174,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
}
}
public static async Task TryAbandonMultiplayerRun(Action updateButtons)
public static async Task TryAbandonMultiplayerRun(NMultiplayerSubmenu instance)
{
var header = new LocString("main_menu_ui", "ABANDON_RUN_CONFIRMATION.header");
var body = new LocString("main_menu_ui", "ABANDON_RUN_CONFIRMATION.body");
@@ -209,7 +220,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
}
LanRunSaveManagerService.Instance.DeleteCurrentMultiplayerRun();
updateButtons();
UpdateButtons(instance);
}
}
}
@@ -0,0 +1,31 @@
using MegaCrit.Sts2.Core.Debug;
using MegaCrit.Sts2.Core.Logging;
namespace SlayTheSpire2.LAN.Multiplayer.Helpers
{
internal static class TaskGenericHelper
{
public static Task<T> RunSafely<T>(Task<T> task)
{
return LogTaskExceptions(task);
}
private static async Task<T> LogTaskExceptions<T>(Task<T> task)
{
try
{
return await task;
}
catch (Exception ex)
{
if (ex is not TaskCanceledException)
{
Log.Error(ex.ToString());
SentryService.CaptureException(ex);
}
throw;
}
}
}
}
@@ -1,6 +1,7 @@
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Multiplayer.Serialization;
using MegaCrit.Sts2.Core.Multiplayer.Transport;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
namespace SlayTheSpire2.LAN.Multiplayer.Models
{
@@ -14,7 +15,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Models
public void Serialize(PacketWriter writer)
{
writer.WriteInt(playerNames.Count);
PacketHelper.WriteVarInt(writer, (uint)playerNames.Count);
foreach (var keyValue in playerNames)
{
writer.WriteULong(keyValue.Key);
@@ -24,7 +25,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Models
public void Deserialize(PacketReader reader)
{
var count = reader.ReadInt();
var count = PacketHelper.ReadVarInt(reader);
playerNames = new PlayerNames();
for (var i = 0; i < count; i++)
{
@@ -0,0 +1,127 @@
using HarmonyLib;
using MegaCrit.Sts2.Core.Context;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Multiplayer;
using MegaCrit.Sts2.Core.Multiplayer.Game;
using MegaCrit.Sts2.Core.Multiplayer.Game.Lobby;
using MegaCrit.Sts2.Core.Multiplayer.Messages.Game;
using MegaCrit.Sts2.Core.Runs;
using MegaCrit.Sts2.Core.Saves.Runs;
using Logger = MegaCrit.Sts2.Core.Logging.Logger;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
[HarmonyPatch(typeof(CombatStateSynchronizer), "WaitForSync")]
internal class CombatStateSynchronizerWaitForSyncPatch
{
private static bool Prefix(CombatStateSynchronizer __instance, Logger ____logger,
INetGameService ____netService, TaskCompletionSource? ____syncCompletionSource,
Dictionary<ulong, SerializablePlayer> ____syncData, RunState ____runState, RunLobby? ____runLobby,
SerializableRunRngSet? ____rngSet, SerializableRelicGrabBag? ____sharedRelicGrabBag, ref Task __result)
{
//Whether is LAN game was not checked, because the sync issue may also occur when connect via Steam
__result = TaskHelper.RunSafely(WaitForSync(__instance, ____logger, ____netService,
____syncCompletionSource, ____syncData, ____runState, ____runLobby, ____rngSet,
____sharedRelicGrabBag));
return false;
}
private static async Task WaitForSync(CombatStateSynchronizer instance, Logger logger,
INetGameService netService, TaskCompletionSource? syncCompletionSource,
Dictionary<ulong, SerializablePlayer> syncData, RunState runState, RunLobby? runLobby,
SerializableRunRngSet? rngSet, SerializableRelicGrabBag? sharedRelicGrabBag)
{
logger.Debug("Waiting to receive all sync messages from all clients");
if (netService.Type == NetGameType.Singleplayer || instance.IsDisabled)
return;
if (syncCompletionSource == null)
{
throw new InvalidOperationException("StartSync must be called before WaitForSync!");
}
var startTime = DateTime.Now;
var lastResendTick = DateTime.Now;
const int timeoutSeconds = 30;
const int resendIntervalSeconds = 5;
while (!syncCompletionSource.Task.IsCompleted)
{
if ((DateTime.Now - startTime).TotalSeconds > timeoutSeconds)
{
logger.Warn("Receive all sync messages timeout, skip waiting for all clients");
break;
}
if (netService.Type == NetGameType.Host &&
(DateTime.Now - lastResendTick).TotalSeconds > resendIntervalSeconds && rngSet != null &&
sharedRelicGrabBag != null)
{
logger.Debug("Resend rng sync message");
var message = new SyncRngMessage
{
rng = rngSet,
sharedRelicGrabBag = sharedRelicGrabBag
};
netService.SendMessage(message);
lastResendTick = DateTime.Now;
}
await Task.Delay(100);
}
foreach (var syncDatum in syncData)
{
if (runLobby != null && !runLobby.ConnectedPlayerIds.Contains(syncDatum.Key))
{
logger.Debug($"Skipping sync for disconnected player {syncDatum.Key}");
continue;
}
var player = runState.GetPlayer(syncDatum.Key);
if (!LocalContext.IsMe(player))
{
player?.SyncWithSerializedPlayer(syncDatum.Value);
}
}
if (netService.Type != NetGameType.Host)
{
if (rngSet != null)
{
runState.Rng.LoadFromSerializable(rngSet);
}
else if (runState.Players.Count > 1)
{
logger.Error(
"There are two or more players and we are a client, but we never received the RNG set!");
}
if (sharedRelicGrabBag != null)
{
runState.SharedRelicGrabBag.LoadFromSerializable(sharedRelicGrabBag);
}
else if (runState.Players.Count > 1)
{
logger.Error(
"There are two or more players and we are a client, but we never received the shared relic grab bag!");
}
}
syncData.Clear();
var traverse = Traverse.Create(instance);
traverse.Field("_rngSet").SetValue(null);
traverse.Field("_sharedRelicGrabBag").SetValue(null);
traverse.Field("_syncCompletionSource").SetValue(null);
}
}
}
@@ -1,7 +1,6 @@
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.Core.Entities.Multiplayer;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Multiplayer.Transport;
using MegaCrit.Sts2.Core.Multiplayer.Transport.ENet;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
@@ -23,9 +22,10 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.ENet
}
private static bool Prefix(ENetClient __instance, ulong netId, string ip, ushort port,
CancellationToken cancelToken, Logger ____logger, INetClientHandler ____handler, ref Task __result)
CancellationToken cancelToken, Logger ____logger, INetClientHandler ____handler,
ref Task<NetErrorInfo?> __result)
{
__result = TaskHelper.RunSafely(ConnectToHost(__instance, ____logger, ____handler, netId, ip, port,
__result = TaskGenericHelper.RunSafely(ConnectToHost(__instance, ____logger, ____handler, netId, ip, port,
cancelToken));
return false;
@@ -4,6 +4,7 @@ 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.Helpers;
using SlayTheSpire2.LAN.Multiplayer.Models;
using SlayTheSpire2.LAN.Multiplayer.Services;
@@ -17,7 +18,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
private static void Postfix(JoinFlow __instance, ref Task<ClientLobbyJoinResponseMessage> __result)
{
__result = AttemptJoin(__instance, __result);
__result = TaskGenericHelper.RunSafely(AttemptJoin(__instance, __result));
}
private static async Task<ClientLobbyJoinResponseMessage> AttemptJoin(JoinFlow joinFlow,
@@ -44,7 +45,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
private static void Postfix(JoinFlow __instance, ref Task<ClientLoadJoinResponseMessage> __result)
{
__result = AttemptLoadJoin(__instance, __result);
__result = TaskGenericHelper.RunSafely(AttemptLoadJoin(__instance, __result));
}
private static async Task<ClientLoadJoinResponseMessage> AttemptLoadJoin(JoinFlow joinFlow,
@@ -71,7 +72,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
private static void Postfix(JoinFlow __instance, ref Task<ClientRejoinResponseMessage> __result)
{
__result = AttemptRejoin(__instance, __result);
__result = TaskGenericHelper.RunSafely(AttemptRejoin(__instance, __result));
}
private static async Task<ClientRejoinResponseMessage> AttemptRejoin(JoinFlow joinFlow,
@@ -1,5 +1,16 @@
using HarmonyLib;
using MegaCrit.Sts2.Core.Achievements;
using MegaCrit.Sts2.Core.Context;
using MegaCrit.Sts2.Core.Daily;
using MegaCrit.Sts2.Core.Entities.Players;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Models;
using MegaCrit.Sts2.Core.Multiplayer.Game;
using MegaCrit.Sts2.Core.Platform;
using MegaCrit.Sts2.Core.Rooms;
using MegaCrit.Sts2.Core.Runs;
using MegaCrit.Sts2.Core.Runs.Metrics;
using MegaCrit.Sts2.Core.Saves;
using SlayTheSpire2.LAN.Multiplayer.Services;
// ReSharper disable UnusedMember.Global
@@ -16,4 +27,137 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
LanPlayerNameService.Instance.SetDefaultPlayerNames();
}
}
[HarmonyPatch(typeof(RunManager), "OnEnded")]
internal class RunManagerOnEndedPatch
{
[HarmonyReversePatch]
[HarmonyPatch(typeof(RunManager), "UpdatePlayerStatsInMapPointHistory")]
private static void UpdatePlayerStatsInMapPointHistory(RunManager instance)
{
throw new NotImplementedException();
}
[HarmonyReversePatch]
[HarmonyPatch(typeof(RunManager), "CheckUpdateEnemyDiscoveryAfterLoss")]
private static void CheckUpdateEnemyDiscoveryAfterLoss(Player player, ModelId monster)
{
throw new NotImplementedException();
}
private static bool Prefix(RunManager __instance, bool isVictory, ref bool ____runHistoryWasUploaded,
ref SerializableRun __result)
{
if (__instance.NetService.Platform == PlatformType.None)
{
UpdatePlayerStatsInMapPointHistory(__instance);
var state = Traverse.Create(__instance).Property("State").GetValue<RunState?>();
if (state == null)
{
throw new Exception($"{nameof(state)} is null");
}
var me = LocalContext.GetMe(state);
if (me == null)
{
throw new Exception($"{nameof(me)} is null");
}
if (state is { CurrentRoom: CombatRoom combatRoom, CurrentMapPointHistoryEntry: not null })
{
state.CurrentMapPointHistoryEntry.Rooms.Last().TurnsTaken = combatRoom.CombatState.RoundNumber;
}
var serializableRun = __instance.ToSave(null);
var me2 = LocalContext.GetMe(serializableRun);
if (me2 == null)
{
throw new Exception($"{nameof(me2)} is null");
}
if (____runHistoryWasUploaded)
{
__result = serializableRun;
return false;
}
____runHistoryWasUploaded = true;
if (!isVictory && state.CurrentRoom is CombatRoom combatRoom2)
{
foreach (var monstersWithSlot in combatRoom2.Encounter.MonstersWithSlots)
{
var item = monstersWithSlot.Item1;
CheckUpdateEnemyDiscoveryAfterLoss(me, item.Id);
}
}
if (__instance.ShouldSave)
{
using (SaveManager.Instance.BeginSaveBatch())
{
SaveManager.Instance.UpdateProgressWithRunData(serializableRun, isVictory);
foreach (var discoveredEpoch in me2.DiscoveredEpochs)
{
if (!me.DiscoveredEpochs.Contains(discoveredEpoch))
{
me.DiscoveredEpochs.Add(discoveredEpoch);
}
}
AchievementsHelper.AfterRunEnded(state, me, isVictory);
RunHistoryUtilities.CreateRunHistoryEntry(serializableRun, isVictory, __instance.IsAbandoned,
__instance.NetService.Platform);
MetricUtilities.UploadRunMetrics(serializableRun, isVictory, __instance.NetService.NetId);
if (SaveManager.Instance.Progress.NumberOfRuns == 5)
{
MetricUtilities.UploadSettingsMetric();
}
switch (__instance.NetService.Type)
{
case NetGameType.Singleplayer:
SaveManager.Instance.DeleteCurrentRun();
break;
case NetGameType.Host:
LanRunSaveManagerService.Instance.DeleteCurrentMultiplayerRun();
break;
case NetGameType.None:
case NetGameType.Client:
case NetGameType.Replay:
default:
break;
}
}
if (isVictory)
{
var score = ScoreUtility.CalculateScore(serializableRun, isVictory);
StatsManager.IncrementArchitectDamage(score);
}
}
if (__instance.DailyTime.HasValue)
{
var type = __instance.NetService.Type;
if ((uint)(type - 1) <= 1u)
{
var score2 = ScoreUtility.CalculateScore(serializableRun, isVictory);
TaskHelper.RunSafely(DailyRunUtility.UploadScore(__instance.DailyTime.Value, score2,
serializableRun.Players));
}
else if (__instance.NetService.Type == NetGameType.Client)
{
TaskHelper.RunSafely(DailyRunUtility.UploadScore(__instance.DailyTime.Value, -99999,
serializableRun.Players));
}
}
__result = serializableRun;
return false;
}
return true;
}
}
}
@@ -3,6 +3,7 @@ using HarmonyLib;
using MegaCrit.Sts2.Core.Assets;
using MegaCrit.Sts2.Core.Entities.Players;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Localization;
using MegaCrit.Sts2.Core.Nodes.Multiplayer;
using MegaCrit.Sts2.Core.Nodes.Screens.CardLibrary;
using MegaCrit.Sts2.Core.Nodes.Screens.Map;
@@ -36,16 +37,18 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
disableDrawing.Name = "DisableDrawing";
disableDrawing.SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter;
disableDrawing.SetLabel("Disable drawing");
var marginContainer = (MarginContainer)container.GetNode("MarginContainer");
marginContainer.RemoveThemeConstantOverride("margin_top");
var lanMapDrawingsService = LanMapDrawingsService.Instance;
container.AddChildSafely(disableDrawing);
container.MoveChild(disableDrawing, 0);
disableDrawing.SetLabel(new LocString("gameplay_ui", "SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING")
.GetFormattedText());
var lanMapDrawingsService = LanMapDrawingsService.Instance;
disableDrawing.IsTicked =
lanMapDrawingsService.DisableDrawingHashSet.Contains(____player.NetId);
@@ -18,13 +18,6 @@ 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");
@@ -105,8 +98,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
lanAbandonButton.Connect(NClickableControl.SignalName.Released,
Callable.From<NButton>(_ =>
{
TaskHelper.RunSafely(
LanHostHelper.TryAbandonMultiplayerRun(() => UpdateButtons(__instance)));
TaskHelper.RunSafely(LanHostHelper.TryAbandonMultiplayerRun(__instance));
}));
LanMultiplayerSubmenuButtonService.Instance.LanAbandonButton = lanAbandonButton;
@@ -1,5 +1,6 @@
using Godot;
using HarmonyLib;
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Nodes.Screens.Overlays;
// ReSharper disable UnusedMember.Global
@@ -7,23 +8,21 @@ using MegaCrit.Sts2.Core.Nodes.Screens.Overlays;
namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{
[HarmonyPatch("MegaCrit.Sts2.Core.Nodes.Screens.Overlays.NOverlayStack", "Remove")]
[HarmonyPatch(typeof(NOverlayStack), "Remove")]
internal class NOverlayStackRemovePatch
{
private static bool Prefix(object __instance, object screen)
{
var overlay = screen as GodotObject;
if (overlay == null || !GodotObject.IsInstanceValid(overlay))
private static bool Prefix(NOverlayStack __instance, IOverlayScreen screen)
{
if (screen is not GodotObject overlay || !GodotObject.IsInstanceValid(overlay))
return false;
}
var connections = overlay.GetSignalConnectionList("Completed");
foreach (var connection in connections)
{
if (connection["callable"].AsCallable().Target == __instance)
{
GD.Print("NOverlayStack.Remove called twice, preventing crash.");
Log.Debug("NOverlayStack.Remove called twice, preventing crash.");
return false;
}
}
@@ -7,6 +7,7 @@ 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.Helpers;
using SlayTheSpire2.LAN.Multiplayer.Services;
// ReSharper disable UnusedMember.Global
@@ -97,7 +98,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{
if (____lobby.NetService.Platform == PlatformType.None)
{
__result = RunScreenService.ShouldAllowRunToBegin(____lobby);
__result = TaskGenericHelper.RunSafely(RunScreenService.ShouldAllowRunToBegin(____lobby));
return false;
}
@@ -112,7 +113,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs.Screens
{
if (____runLobby.NetService.Platform == PlatformType.None)
{
__result = RunScreenService.ShouldAllowRunToBegin(____runLobby);
__result = TaskGenericHelper.RunSafely(RunScreenService.ShouldAllowRunToBegin(____runLobby));
return false;
}
@@ -4,8 +4,8 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyVersion>1.6.1.0</AssemblyVersion>
<FileVersion>1.6.1.0</FileVersion>
<AssemblyVersion>1.6.3.0</AssemblyVersion>
<FileVersion>1.6.3.0</FileVersion>
</PropertyGroup>
<ItemGroup>
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Zeichnen deaktivieren"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Disable drawing"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Deshabilitar dibujo"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Désactiver le dessin"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Disabilita il disegno"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "描画を無効にする"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "그리기 비활성화"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Wyłącz rysowanie"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Desativar desenho"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Отключить рисование"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Deshabilitar dibujo"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "ปิดใช้งานการวาดภาพ"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "Çizimi devre dışı bırak"
}
+3
View File
@@ -0,0 +1,3 @@
{
"SlayTheSpire2.LAN.Multiplayer.DISABLE_DRAWING": "禁用绘图"
}