Compare commits
116 Commits
a9c14c3adf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b9c19186b | |||
| 99398b5479 | |||
| 270a92c617 | |||
| 4d8279719c | |||
| 3d5a53f0e2 | |||
| 25f5af0b4d | |||
| 2ad605138e | |||
| ca517be369 | |||
| df1def69e4 | |||
| db850885f6 | |||
| 71e1e09c75 | |||
| 46ebeb1f80 | |||
| 7d06145a34 | |||
| fcf1ffac76 | |||
| 40369ffe4b | |||
| fc089afcc0 | |||
| cb82d3706f | |||
| 12a207989e | |||
| 4afcaccf75 | |||
| 8f13282e04 | |||
| 0d7d989f76 | |||
| 1cc3042781 | |||
| 181936e3d4 | |||
| 62db70c1c4 | |||
| 36acbb57c5 | |||
| 76f1033bd2 | |||
| 26b5f1201e | |||
| 9ee675ac19 | |||
| 3124f82a2f | |||
| 3a7b81bfd7 | |||
| c4eb7e944d | |||
| 4057cf37c5 | |||
| fc467f4af8 | |||
| dbc0328c6f | |||
| 5f67f488f6 | |||
| b0f790cb6e | |||
| deb0c0ad92 | |||
| 10d20e29ad | |||
| a3409a2d5c | |||
| 5b0f957295 | |||
| faaedcc0fd | |||
| d87c12384e | |||
| 560a985323 | |||
| 490e0d74e6 | |||
| c4e25f6c12 | |||
| 6b014c72db | |||
| c6ca1abc54 | |||
| c3e79c9adf | |||
| dca0140aab | |||
| 55e3cd57e7 | |||
| 4685bc61c2 | |||
| 7a14303353 | |||
| a918262d99 | |||
| 87f65320c0 | |||
| 97bbf059a9 | |||
| aa6fb797c2 | |||
| 739eba8289 | |||
| 1e6f09df18 | |||
| 15cad47b52 | |||
| c074a86423 | |||
| 1035f06884 | |||
| b05c3415f2 | |||
| b02af4d0c7 | |||
| 5dba8137c3 | |||
| b3d7108574 | |||
| ce1c067fca | |||
| 5d086776cf | |||
| 89586ad8df | |||
| 8f3d83e807 | |||
| dd17030e56 | |||
| 8d599e13ad | |||
| eab7931f52 | |||
| 7d6c915b49 | |||
| f7fc5a3969 | |||
| 2f42cf9366 | |||
| 888c807b96 | |||
| bd12485112 | |||
| f03e13236f | |||
| 60d509344c | |||
| 121df60b57 | |||
| 3fbaac2346 | |||
| 0e87108b56 | |||
| 224ba3f912 | |||
| 5ebd94c03a | |||
| 914650c211 | |||
| 93c55dd482 | |||
| 414ab90afc | |||
| 42a86419ca | |||
| 2140fc3868 | |||
| 4a2c73badb | |||
| d6c0ec2a33 | |||
| 634a5f7983 | |||
| 77f4d4fed0 | |||
| 3ee6fc4dc6 | |||
| e0b1b736c3 | |||
| 28e342b1e3 | |||
| 4871f7c150 | |||
| 3dcb9a85b5 | |||
| e91ae0fc99 | |||
| 8edea198cd | |||
| f82ae76a3e | |||
| dc50bf2892 | |||
| e636ad6e19 | |||
| fbb947a23b | |||
| a2d87106ba | |||
| 0f8f3ce818 | |||
| 07c7e14cde | |||
| 0549ab0e19 | |||
| f45402af9a | |||
| 6bf74dda43 | |||
| 8ac2f2df1c | |||
| b351e0c707 | |||
| 0776da883f | |||
| cb6fd3a727 | |||
| 6a128a836d | |||
| 5fa2cc1c92 |
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(wc:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(tail:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
# Logs / local debug output
|
||||
output*.txt
|
||||
*.log
|
||||
|
||||
# OS junk
|
||||
@@ -21,3 +20,8 @@ Desktop.ini
|
||||
**/obj/
|
||||
**/*.mdb
|
||||
**/*.pdb
|
||||
|
||||
/.claude
|
||||
/*.png
|
||||
/*.txt
|
||||
/*.jpg
|
||||
|
||||
15
Constants.cs
15
Constants.cs
@@ -15,19 +15,20 @@ namespace KCM
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
public static readonly MainMenuMode MainMenuMode = GameState.inst.mainMenuMode;
|
||||
public static readonly PlayingMode PlayingMode = GameState.inst.playingMode;
|
||||
public static readonly World World = GameState.inst.world;
|
||||
// Use lazy initialization to avoid null reference when GameState isn't ready yet
|
||||
public static MainMenuMode MainMenuMode => GameState.inst?.mainMenuMode;
|
||||
public static PlayingMode PlayingMode => GameState.inst?.playingMode;
|
||||
public static World World => GameState.inst?.world;
|
||||
|
||||
#region "UI"
|
||||
public static readonly Transform MainMenuUI_T = MainMenuMode.mainMenuUI.transform;
|
||||
public static readonly GameObject MainMenuUI_O = MainMenuMode.mainMenuUI;
|
||||
public static Transform MainMenuUI_T => MainMenuMode?.mainMenuUI?.transform;
|
||||
public static GameObject MainMenuUI_O => MainMenuMode?.mainMenuUI;
|
||||
|
||||
/* public static readonly Transform TopLevelUI_T = MainMenuUI_T.parent;
|
||||
public static readonly GameObject TopLevelUI_O = MainMenuUI_T.parent.gameObject;*/
|
||||
|
||||
public static readonly Transform ChooseModeUI_T = MainMenuMode.chooseModeUI.transform;
|
||||
public static readonly GameObject ChooseModeUI_O = MainMenuMode.chooseModeUI;
|
||||
public static Transform ChooseModeUI_T => MainMenuMode?.chooseModeUI?.transform;
|
||||
public static GameObject ChooseModeUI_O => MainMenuMode?.chooseModeUI;
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
@@ -18,11 +18,8 @@ namespace KCM.Enums
|
||||
KingdomName = 32,
|
||||
StartGame = 33,
|
||||
WorldSeed = 34,
|
||||
|
||||
|
||||
Building = 50,
|
||||
BuildingOnPlacement = 51,
|
||||
|
||||
World = 70,
|
||||
WorldPlace = 71,
|
||||
FellTree = 72,
|
||||
@@ -44,6 +41,7 @@ namespace KCM.Enums
|
||||
AddVillager = 88,
|
||||
SetupInitialWorkers = 89,
|
||||
VillagerTeleportTo = 90,
|
||||
PlaceKeepRandomly = 91
|
||||
PlaceKeepRandomly = 91,
|
||||
BuildingRemove = 92
|
||||
}
|
||||
}
|
||||
|
||||
17
KCClient.cs
17
KCClient.cs
@@ -36,11 +36,10 @@ namespace KCM
|
||||
|
||||
private static void Client_Disconnected(object sender, DisconnectedEventArgs e)
|
||||
{
|
||||
Main.CleanupMultiplayerSession();
|
||||
Main.helper.Log("Client disconnected event start");
|
||||
try
|
||||
{
|
||||
Main.ResetMultiplayerState("Client disconnected");
|
||||
|
||||
if (e.Message != null)
|
||||
{
|
||||
Main.helper.Log(e.Message.ToString());
|
||||
@@ -78,19 +77,7 @@ namespace KCM
|
||||
|
||||
private static void Client_Connected(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (client != null && client.Connection != null)
|
||||
{
|
||||
client.Connection.CanQualityDisconnect = false;
|
||||
client.Connection.MaxSendAttempts = 50;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error configuring client connection");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
105
KCServer.cs
105
KCServer.cs
@@ -21,9 +21,6 @@ namespace KCM
|
||||
public static Server server = new Server(Main.steamServer);
|
||||
public static bool started = false;
|
||||
|
||||
private static readonly Dictionary<ushort, Queue<SaveTransferPacket>> saveTransferQueues = new Dictionary<ushort, Queue<SaveTransferPacket>>();
|
||||
private const int SaveTransferPacketsPerUpdatePerClient = 10;
|
||||
|
||||
static KCServer()
|
||||
{
|
||||
//server.registerMessageHandler(typeof(KCServer).GetMethod("ClientJoined"));
|
||||
@@ -53,7 +50,6 @@ namespace KCM
|
||||
}
|
||||
|
||||
ev.Client.CanQualityDisconnect = false;
|
||||
ev.Client.MaxSendAttempts = 50;
|
||||
|
||||
Main.helper.Log("Client ID is: " + ev.Client.Id);
|
||||
|
||||
@@ -62,42 +58,15 @@ namespace KCM
|
||||
|
||||
server.ClientDisconnected += (obj, ev) =>
|
||||
{
|
||||
try
|
||||
new ChatSystemMessage()
|
||||
{
|
||||
var playerName = $"Client {ev.Client.Id}";
|
||||
string steamId;
|
||||
if (Main.clientSteamIds.TryGetValue(ev.Client.Id, out steamId) && !string.IsNullOrEmpty(steamId))
|
||||
{
|
||||
KCPlayer player;
|
||||
if (Main.kCPlayers.TryGetValue(steamId, out player) && player != null && !string.IsNullOrEmpty(player.name))
|
||||
playerName = player.name;
|
||||
Message = $"{Main.GetPlayerByClientID(ev.Client.Id).name} has left the server.",
|
||||
}.SendToAll();
|
||||
|
||||
Main.kCPlayers.Remove(steamId);
|
||||
}
|
||||
Main.kCPlayers.Remove(Main.GetPlayerByClientID(ev.Client.Id).steamId);
|
||||
Destroy(LobbyHandler.playerEntries.Select(x => x.GetComponent<PlayerEntryScript>()).Where(x => x.Client == ev.Client.Id).FirstOrDefault().gameObject);
|
||||
|
||||
Main.clientSteamIds.Remove(ev.Client.Id);
|
||||
|
||||
new ChatSystemMessage()
|
||||
{
|
||||
Message = $"{playerName} has left the server.",
|
||||
}.SendToAll();
|
||||
|
||||
var entry = LobbyHandler.playerEntries
|
||||
.Select(x => x != null ? x.GetComponent<PlayerEntryScript>() : null)
|
||||
.FirstOrDefault(x => x != null && x.Client == ev.Client.Id);
|
||||
|
||||
if (entry != null)
|
||||
Destroy(entry.gameObject);
|
||||
|
||||
saveTransferQueues.Remove(ev.Client.Id);
|
||||
|
||||
Main.helper.Log($"Client disconnected. {ev.Reason}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error handling client disconnect");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
Main.helper.Log($"Client disconnected. {ev.Reason}");
|
||||
};
|
||||
|
||||
Main.helper.Log($"Listening on port 7777. Max {LobbyHandler.ServerSettings.MaxPlayers} clients.");
|
||||
@@ -131,68 +100,6 @@ namespace KCM
|
||||
private void Update()
|
||||
{
|
||||
server.Update();
|
||||
ProcessSaveTransfers();
|
||||
}
|
||||
|
||||
private static void ProcessSaveTransfers()
|
||||
{
|
||||
if (!KCServer.IsRunning)
|
||||
return;
|
||||
|
||||
if (saveTransferQueues.Count == 0)
|
||||
return;
|
||||
|
||||
var clients = saveTransferQueues.Keys.ToList();
|
||||
foreach (var clientId in clients)
|
||||
{
|
||||
Queue<SaveTransferPacket> queue;
|
||||
if (!saveTransferQueues.TryGetValue(clientId, out queue) || queue == null)
|
||||
continue;
|
||||
|
||||
int sentThisUpdate = 0;
|
||||
while (sentThisUpdate < SaveTransferPacketsPerUpdatePerClient && queue.Count > 0)
|
||||
{
|
||||
var packet = queue.Dequeue();
|
||||
packet.Send(clientId);
|
||||
sentThisUpdate++;
|
||||
}
|
||||
|
||||
if (queue.Count == 0)
|
||||
saveTransferQueues.Remove(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
public static void EnqueueSaveTransfer(ushort toClient, byte[] bytes)
|
||||
{
|
||||
if (bytes == null)
|
||||
return;
|
||||
|
||||
int chunkSize = 900;
|
||||
int sent = 0;
|
||||
int totalChunks = (int)Math.Ceiling((double)bytes.Length / chunkSize);
|
||||
|
||||
var queue = new Queue<SaveTransferPacket>(totalChunks);
|
||||
for (int i = 0; i < totalChunks; i++)
|
||||
{
|
||||
int currentChunkSize = Math.Min(chunkSize, bytes.Length - sent);
|
||||
var chunk = new byte[currentChunkSize];
|
||||
Array.Copy(bytes, sent, chunk, 0, currentChunkSize);
|
||||
|
||||
queue.Enqueue(new SaveTransferPacket()
|
||||
{
|
||||
saveSize = bytes.Length,
|
||||
saveDataChunk = chunk,
|
||||
chunkId = i,
|
||||
chunkSize = chunk.Length,
|
||||
saveDataIndex = sent,
|
||||
totalChunks = totalChunks
|
||||
});
|
||||
|
||||
sent += currentChunkSize;
|
||||
}
|
||||
|
||||
saveTransferQueues[toClient] = queue;
|
||||
Main.helper.Log($"Queued {totalChunks} save data chunks for client {toClient}");
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
|
||||
@@ -27,36 +27,12 @@ namespace KCM.LoadSaveOverrides
|
||||
//this.PlayerSaveData = new PlayerSaveDataOverride().Pack(Player.inst);
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (player == null)
|
||||
continue;
|
||||
Main.helper.Log($"Attempting to pack data for: " + player.name + $"({player.steamId})");
|
||||
Main.helper.Log($"{player.inst.ToString()} {player.inst?.gameObject.name}");
|
||||
this.players.Add(player.steamId, new Player.PlayerSaveData().Pack(player.inst));
|
||||
kingdomNames.Add(player.steamId, player.kingdomName);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(player.steamId))
|
||||
{
|
||||
Main.helper.Log($"Skipping save for player with missing steamId (name={player.name ?? string.Empty})");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (player.inst == null)
|
||||
{
|
||||
Main.helper.Log($"Skipping save for player {player.name ?? string.Empty} ({player.steamId}) because Player.inst is null");
|
||||
continue;
|
||||
}
|
||||
|
||||
Main.helper.Log($"Attempting to pack data for: {player.name} ({player.steamId})");
|
||||
Main.helper.Log($"Player object: {player.inst} {player.inst.gameObject?.name}");
|
||||
|
||||
this.players[player.steamId] = new Player.PlayerSaveData().Pack(player.inst);
|
||||
kingdomNames[player.steamId] = player.kingdomName ?? " ";
|
||||
|
||||
Main.helper.Log($"{players[player.steamId] == null}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log($"Error packing player data for save (steamId={player?.steamId ?? string.Empty}, name={player?.name ?? string.Empty})");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
Main.helper.Log($"{players[player.steamId] == null}");
|
||||
}
|
||||
|
||||
this.WorldSaveData = new World.WorldSaveData().Pack(World.inst);
|
||||
|
||||
432
Main.cs
432
Main.cs
@@ -55,112 +55,44 @@ namespace KCM
|
||||
public static Dictionary<string, KCPlayer> kCPlayers = new Dictionary<string, KCPlayer>();
|
||||
public static Dictionary<ushort, string> clientSteamIds = new Dictionary<ushort, string>();
|
||||
|
||||
private static readonly Dictionary<int, long> lastTeamIdLookupLogMs = new Dictionary<int, long>();
|
||||
private static int resetInProgress = 0;
|
||||
|
||||
public static void ResetMultiplayerState(string reason = null)
|
||||
{
|
||||
if (Interlocked.Exchange(ref resetInProgress, 1) == 1)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(reason))
|
||||
helper?.Log($"ResetMultiplayerState: {reason}");
|
||||
|
||||
try { StateObserver.ClearAll(); } catch { }
|
||||
try { SaveTransferPacket.ResetTransferState(); } catch { }
|
||||
|
||||
try
|
||||
{
|
||||
LoadSaveLoadHook.memoryStreamHook = false;
|
||||
LoadSaveLoadHook.saveBytes = new byte[0];
|
||||
LoadSaveLoadHook.saveContainer = null;
|
||||
}
|
||||
catch { }
|
||||
|
||||
try { LoadSaveLoadAtPathHook.saveData = new byte[0]; } catch { }
|
||||
|
||||
try { LobbyManager.loadingSave = false; } catch { }
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var player in kCPlayers.Values)
|
||||
{
|
||||
if (player?.gameObject != null && player.gameObject.name != null && player.gameObject.name.Contains("Client Player"))
|
||||
UnityEngine.Object.Destroy(player.gameObject);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
try { LobbyHandler.ClearChatEntries(); } catch { }
|
||||
try { LobbyHandler.ClearPlayerList(); } catch { }
|
||||
|
||||
try
|
||||
{
|
||||
kCPlayers.Clear();
|
||||
clientSteamIds.Clear();
|
||||
}
|
||||
catch { }
|
||||
|
||||
try { lastTeamIdLookupLogMs.Clear(); } catch { }
|
||||
|
||||
try
|
||||
{
|
||||
if (KCClient.client != null && KCClient.client.IsConnected)
|
||||
KCClient.client.Disconnect();
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
if (KCServer.IsRunning)
|
||||
KCServer.server.Stop();
|
||||
}
|
||||
catch { }
|
||||
|
||||
try { ServerBrowser.registerServer = false; } catch { }
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref resetInProgress, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static KCPlayer GetPlayerByClientID(ushort clientId)
|
||||
{
|
||||
return kCPlayers[clientSteamIds[clientId]];
|
||||
if (TryGetPlayerByClientID(clientId, out KCPlayer player))
|
||||
{
|
||||
return player;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool TryGetPlayerByClientID(ushort clientId, out KCPlayer player)
|
||||
{
|
||||
player = null;
|
||||
if (clientSteamIds.TryGetValue(clientId, out string steamId))
|
||||
{
|
||||
return kCPlayers.TryGetValue(steamId, out player);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Player GetPlayerByTeamID(int teamId) // Need to replace building / production types so that the correct player is used. IResourceStorage and IResourceProvider, and jobs
|
||||
{
|
||||
KCPlayer match = kCPlayers.Values.FirstOrDefault(p =>
|
||||
p != null &&
|
||||
p.inst != null &&
|
||||
p.inst.PlayerLandmassOwner != null &&
|
||||
p.inst.PlayerLandmassOwner.teamId == teamId);
|
||||
|
||||
if (match != null && match.inst != null)
|
||||
return match.inst;
|
||||
|
||||
if (KCServer.IsRunning || KCClient.client.IsConnected)
|
||||
try
|
||||
{
|
||||
long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
long last;
|
||||
if (!lastTeamIdLookupLogMs.TryGetValue(teamId, out last) || (now - last) > 2000)
|
||||
var player = kCPlayers.Values.FirstOrDefault(p => p.inst.PlayerLandmassOwner.teamId == teamId).inst;
|
||||
|
||||
return player;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (KCServer.IsRunning || KCClient.client.IsConnected)
|
||||
{
|
||||
lastTeamIdLookupLogMs[teamId] = now;
|
||||
|
||||
string myTeamId = (Player.inst != null && Player.inst.PlayerLandmassOwner != null)
|
||||
? Player.inst.PlayerLandmassOwner.teamId.ToString()
|
||||
: "unknown";
|
||||
|
||||
Main.helper.Log("Failed finding player by teamID: " + teamId + " My teamID is: " + myTeamId);
|
||||
Main.helper.Log("Failed finding player by teamID: " + teamId + " My teamID is: " + Player.inst.PlayerLandmassOwner.teamId);
|
||||
Main.helper.Log(kCPlayers.Count.ToString());
|
||||
Main.helper.Log(string.Join(", ", kCPlayers.Values.Where(p => p != null && p.inst != null && p.inst.PlayerLandmassOwner != null).Select(p => p.inst.PlayerLandmassOwner.teamId.ToString())));
|
||||
Main.helper.Log(string.Join(", ", kCPlayers.Values.Select(p => p.inst.PlayerLandmassOwner.teamId.ToString())));
|
||||
Main.helper.Log(e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
return Player.inst;
|
||||
}
|
||||
|
||||
@@ -188,11 +120,51 @@ namespace KCM
|
||||
public static string PlayerSteamID = SteamUser.GetSteamID().ToString();
|
||||
|
||||
public static KCMSteamManager KCMSteamManager = null;
|
||||
public static LobbyManager lobbyManager = null;
|
||||
public static SteamServer steamServer = new SteamServer();
|
||||
public static Riptide.Transports.Steam.SteamClient steamClient = new Riptide.Transports.Steam.SteamClient(steamServer);
|
||||
|
||||
public static ushort currentClient = 0;
|
||||
|
||||
public static void CleanupMultiplayerSession()
|
||||
{
|
||||
if (helper == null) return; // Avoid running if mod is not fully initialized
|
||||
|
||||
helper.Log("--- Starting Multiplayer Session Cleanup ---");
|
||||
|
||||
// Disconnect client
|
||||
if (KCClient.client != null && KCClient.client.IsConnected)
|
||||
{
|
||||
helper.Log("Disconnecting client...");
|
||||
KCClient.client.Disconnect();
|
||||
}
|
||||
|
||||
// Stop server
|
||||
if (KCServer.server != null && KCServer.IsRunning)
|
||||
{
|
||||
helper.Log("Stopping server...");
|
||||
KCServer.server.Stop();
|
||||
}
|
||||
|
||||
// Clear player lists
|
||||
if (kCPlayers.Count > 0 || clientSteamIds.Count > 0)
|
||||
{
|
||||
helper.Log($"Clearing {kCPlayers.Count} KCPlayer entries and {clientSteamIds.Count} client steam IDs.");
|
||||
kCPlayers.Clear();
|
||||
clientSteamIds.Clear();
|
||||
}
|
||||
|
||||
// Destroy persistent managers
|
||||
if (lobbyManager != null)
|
||||
{
|
||||
helper.Log("Destroying LobbyManager.");
|
||||
Destroy(lobbyManager.gameObject);
|
||||
lobbyManager = null;
|
||||
}
|
||||
|
||||
helper.Log("--- Multiplayer Session Cleanup Finished ---");
|
||||
}
|
||||
|
||||
#region "SceneLoaded"
|
||||
private void SceneLoaded(KCModHelper helper)
|
||||
{
|
||||
@@ -205,14 +177,9 @@ namespace KCM
|
||||
KCMSteamManager = new GameObject("KCMSteamManager").AddComponent<KCMSteamManager>();
|
||||
DontDestroyOnLoad(KCMSteamManager);
|
||||
|
||||
var lobbyManager = new GameObject("LobbyManager").AddComponent<LobbyManager>();
|
||||
lobbyManager = new GameObject("LobbyManager").AddComponent<LobbyManager>();
|
||||
DontDestroyOnLoad(lobbyManager);
|
||||
|
||||
//SteamFriends.InviteUserToGame(new CSteamID(76561198036307537), "test");
|
||||
//SteamMatchmaking.lobby
|
||||
|
||||
//Main.helper.Log($"Timer duration for hazardpay {Player.inst.hazardPayWarmup.Duration}");
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -222,24 +189,62 @@ namespace KCM
|
||||
|
||||
Main.helper.Log(JsonConvert.SerializeObject(World.inst.mapSizeDefs, Formatting.Indented));
|
||||
|
||||
KaC_Button serverBrowser = new KaC_Button(Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").parent)
|
||||
// Check if MainMenuUI_T is available
|
||||
if (Constants.MainMenuUI_T == null)
|
||||
{
|
||||
Main.helper.Log("MainMenuUI_T is null, cannot create Multiplayer button");
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug: Log the UI structure to find the correct path
|
||||
Main.helper.Log($"MainMenuUI_T name: {Constants.MainMenuUI_T.name}");
|
||||
Main.helper.Log($"MainMenuUI_T children count: {Constants.MainMenuUI_T.childCount}");
|
||||
for (int i = 0; i < Constants.MainMenuUI_T.childCount; i++)
|
||||
{
|
||||
var child = Constants.MainMenuUI_T.GetChild(i);
|
||||
Main.helper.Log($" Child {i}: {child.name}");
|
||||
for (int j = 0; j < child.childCount; j++)
|
||||
{
|
||||
Main.helper.Log($" SubChild {j}: {child.GetChild(j).name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Correct path based on debug output: MainMenuUI -> TopLevelUICanvas -> TopLevel -> Body -> ButtonContainer -> New
|
||||
var buttonContainer = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New");
|
||||
if (buttonContainer == null)
|
||||
{
|
||||
Main.helper.Log("Button container not found at TopLevelUICanvas/TopLevel/Body/ButtonContainer/New");
|
||||
return;
|
||||
}
|
||||
Main.helper.Log($"Found button container at: {buttonContainer.name}");
|
||||
|
||||
var templateButton = buttonContainer.GetComponent<Button>();
|
||||
if (templateButton == null)
|
||||
{
|
||||
Main.helper.Log("Template button on container is missing Button component.");
|
||||
return;
|
||||
}
|
||||
|
||||
KaC_Button serverBrowser = new KaC_Button(templateButton, buttonContainer.parent)
|
||||
{
|
||||
Name = "Multiplayer",
|
||||
Text = "Multiplayer",
|
||||
FirstSibling = true,
|
||||
OnClick = () =>
|
||||
{
|
||||
//Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel").gameObject.SetActive(false);
|
||||
Main.helper?.Log("Multiplayer button clicked");
|
||||
SfxSystem.PlayUiSelect();
|
||||
|
||||
//ServerBrowser.serverBrowserRef.SetActive(true);
|
||||
TransitionTo(MenuState.ServerBrowser);
|
||||
}
|
||||
};
|
||||
serverBrowser.Transform.SetSiblingIndex(2);
|
||||
|
||||
|
||||
Destroy(Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/Kingdom Share").gameObject);
|
||||
var kingdomShare = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/Kingdom Share")
|
||||
?? Constants.MainMenuUI_T.Find("MainMenu/TopLevel/Body/ButtonContainer/Kingdom Share");
|
||||
if (kingdomShare != null)
|
||||
{
|
||||
Destroy(kingdomShare.gameObject);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -306,11 +311,22 @@ namespace KCM
|
||||
{
|
||||
try
|
||||
{
|
||||
ServerBrowser.serverBrowserRef.SetActive(state == MenuState.ServerBrowser);
|
||||
ServerBrowser.serverLobbyRef.SetActive(state == MenuState.ServerLobby);
|
||||
// Null checks for ServerBrowser references
|
||||
if (ServerBrowser.serverBrowserRef != null)
|
||||
ServerBrowser.serverBrowserRef.SetActive(state == MenuState.ServerBrowser);
|
||||
|
||||
ServerBrowser.KCMUICanvas.gameObject.SetActive((int)state > 21);
|
||||
helper.Log(((int)state > 21).ToString());
|
||||
if (ServerBrowser.serverLobbyRef != null)
|
||||
ServerBrowser.serverLobbyRef.SetActive(state == MenuState.ServerLobby);
|
||||
|
||||
if (ServerBrowser.KCMUICanvas != null)
|
||||
{
|
||||
ServerBrowser.KCMUICanvas.gameObject.SetActive((int)state > 21);
|
||||
if (state == MenuState.ServerBrowser)
|
||||
{
|
||||
Main.helper?.Log($"TransitionTo ServerBrowser: browserRef={(ServerBrowser.serverBrowserRef != null ? "ready" : "null")}, canvas={(ServerBrowser.KCMUICanvas != null ? "ready" : "null")}");
|
||||
}
|
||||
helper.Log(((int)state > 21).ToString());
|
||||
}
|
||||
|
||||
GameState.inst.mainMenuMode.TransitionTo((MainMenuMode.State)state);
|
||||
}
|
||||
@@ -340,9 +356,6 @@ namespace KCM
|
||||
helper.Log("Preload start in main");
|
||||
try
|
||||
{
|
||||
|
||||
|
||||
//MainMenuPatches.Patch();
|
||||
Main.helper = helper;
|
||||
helper.Log(helper.modPath);
|
||||
|
||||
@@ -380,9 +393,6 @@ namespace KCM
|
||||
|
||||
if (newState != MainMenuMode.State.Uninitialized)
|
||||
Main.menuState = (MenuState)newState;
|
||||
|
||||
if ((MenuState)newState == MenuState.Menu && (KCClient.client.IsConnected || KCServer.IsRunning))
|
||||
ResetMultiplayerState("Returned to main menu");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,6 +487,8 @@ namespace KCM
|
||||
{
|
||||
public static void Postfix()
|
||||
{
|
||||
// Your code here
|
||||
|
||||
// Get the name of the last method that called OnPlayerPlacement
|
||||
List<string> strings = new List<string>();
|
||||
|
||||
@@ -623,12 +635,7 @@ namespace KCM
|
||||
{
|
||||
public static bool Prefix(Player __instance)
|
||||
{
|
||||
var localPlayer = Player.inst;
|
||||
if (KCClient.client.IsConnected &&
|
||||
__instance != null &&
|
||||
(localPlayer == null || __instance != localPlayer) &&
|
||||
__instance.gameObject != null &&
|
||||
__instance.gameObject.name.Contains("Client Player"))
|
||||
if (KCClient.client.IsConnected && __instance.gameObject.name.Contains("Client Player") && !LobbyManager.loadingSave)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -1284,7 +1291,6 @@ namespace KCM
|
||||
if (KCServer.IsRunning)
|
||||
{
|
||||
Main.helper.Log("Trying to load multiplayer save");
|
||||
KCM.StateManagement.Observers.StateObserver.ClearAll();
|
||||
LoadSave.LastLoadDirectory = path;
|
||||
path = path + "/" + filename;
|
||||
|
||||
@@ -1294,18 +1300,11 @@ namespace KCM
|
||||
{
|
||||
BinaryFormatter bf = new BinaryFormatter();
|
||||
bf.Binder = new MultiplayerSaveDeserializationBinder();
|
||||
saveData = File.ReadAllBytes(path);
|
||||
Stream file = new FileStream(path, FileMode.Open);
|
||||
try
|
||||
{
|
||||
object deserialized = bf.Deserialize(file);
|
||||
MultiplayerSaveContainer loadData = deserialized as MultiplayerSaveContainer;
|
||||
if (loadData == null)
|
||||
{
|
||||
Main.helper.Log("Selected save is not a MultiplayerSaveContainer; falling back to vanilla load.");
|
||||
return true;
|
||||
}
|
||||
|
||||
saveData = File.ReadAllBytes(path);
|
||||
MultiplayerSaveContainer loadData = (MultiplayerSaveContainer)bf.Deserialize(file);
|
||||
loadData.Unpack(null);
|
||||
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
|
||||
}
|
||||
@@ -1350,14 +1349,35 @@ namespace KCM
|
||||
{
|
||||
Main.helper.Log("Attempting to load save from server");
|
||||
|
||||
using (MemoryStream ms = new MemoryStream(saveBytes))
|
||||
try
|
||||
{
|
||||
BinaryFormatter bf = new BinaryFormatter();
|
||||
bf.Binder = new MultiplayerSaveDeserializationBinder();
|
||||
saveContainer = (MultiplayerSaveContainer)bf.Deserialize(ms);
|
||||
using (MemoryStream ms = new MemoryStream(saveBytes))
|
||||
{
|
||||
BinaryFormatter bf = new BinaryFormatter();
|
||||
bf.Binder = new MultiplayerSaveDeserializationBinder();
|
||||
saveContainer = (MultiplayerSaveContainer)bf.Deserialize(ms);
|
||||
}
|
||||
|
||||
Main.helper.Log("Deserialize complete, calling Unpack...");
|
||||
saveContainer.Unpack(null);
|
||||
Main.helper.Log("Unpack complete!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error loading save from server");
|
||||
Main.helper.Log(e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
if (e.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("Inner exception: " + e.InnerException.Message);
|
||||
Main.helper.Log(e.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
memoryStreamHook = false;
|
||||
}
|
||||
|
||||
memoryStreamHook = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1415,33 +1435,19 @@ namespace KCM
|
||||
{
|
||||
Directory.CreateDirectory(LoadSave.GetSaveDir());
|
||||
Guid guid = Guid.NewGuid();
|
||||
bool hasOverride = !string.IsNullOrWhiteSpace(pathOverride);
|
||||
string path = hasOverride ? pathOverride : Path.Combine(LoadSave.GetSaveDir(), guid.ToString());
|
||||
if (hasOverride && !Path.IsPathRooted(path))
|
||||
path = Path.Combine(LoadSave.GetSaveDir(), pathOverride);
|
||||
string path = (pathOverride != "") ? pathOverride : (LoadSave.GetSaveDir() + "/" + guid);
|
||||
Directory.CreateDirectory(path);
|
||||
Thread thread;
|
||||
try
|
||||
{
|
||||
thread = new Thread(new ParameterizedThreadStart(OutToFile));
|
||||
|
||||
MultiplayerSaveContainer packedData;
|
||||
try
|
||||
{
|
||||
packedData = new MultiplayerSaveContainer().Pack(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Failed to pack multiplayer save data; falling back to vanilla save.");
|
||||
Main.helper.Log(ex.ToString());
|
||||
__result = null;
|
||||
return true;
|
||||
}
|
||||
MultiplayerSaveContainer packedData = new MultiplayerSaveContainer().Pack(null);
|
||||
Broadcast.OnSaveEvent.Broadcast(new OnSaveEvent());
|
||||
thread.Start(new OutData
|
||||
{
|
||||
LoadSaveContainer = packedData,
|
||||
Path = Path.Combine(path, "world")
|
||||
Path = path + "/world"
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -1473,7 +1479,7 @@ namespace KCM
|
||||
|
||||
try
|
||||
{
|
||||
World.inst.TakeScreenshot(Path.Combine(path, "cover"), new Func<int, int, Texture2D>(World.inst.Func_CaptureWorldShot), onCompleteCallback);
|
||||
World.inst.TakeScreenshot(path + "/cover", new Func<int, int, Texture2D>(World.inst.Func_CaptureWorldShot), onCompleteCallback);
|
||||
}
|
||||
catch (Exception e3)
|
||||
{
|
||||
@@ -1542,38 +1548,13 @@ namespace KCM
|
||||
Main.helper.Log($"loading building: {building.FriendlyName}");
|
||||
Main.helper.Log($" (teamid: {building.TeamID()})");
|
||||
Main.helper.Log(p.ToString());
|
||||
|
||||
try
|
||||
bool flag2 = building.GetComponent<Keep>() != null && building.TeamID() == p.PlayerLandmassOwner.teamId;
|
||||
Main.helper.Log("Set keep? " + flag2);
|
||||
if (flag2)
|
||||
{
|
||||
p.PlayerLandmassOwner.TakeOwnership(building.LandMass());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Failed setting landmass ownership during load");
|
||||
Main.helper.Log(e.Message);
|
||||
}
|
||||
|
||||
var keep = building.GetComponent<Keep>();
|
||||
bool shouldSetKeep = keep != null && p.keep == null;
|
||||
Main.helper.Log("Set keep? " + shouldSetKeep);
|
||||
if (shouldSetKeep)
|
||||
{
|
||||
p.keep = keep;
|
||||
p.keep = building.GetComponent<Keep>();
|
||||
Main.helper.Log(p.keep.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
World.inst.PlaceFromLoad(building);
|
||||
structureData.UnpackStage2(building);
|
||||
building.SetVisibleForFog(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error placing building into world during load");
|
||||
Main.helper.Log(e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
}
|
||||
__result = building;
|
||||
}
|
||||
else
|
||||
@@ -1604,6 +1585,16 @@ namespace KCM
|
||||
Main.helper.Log("Saving player creativeMode");
|
||||
__instance.creativeMode = p.creativeMode;
|
||||
|
||||
//cmo options not used for saving or loading in multiplayer
|
||||
/**for (int i = 0; i < p.cmoOptionsOn.Length; i++)
|
||||
{
|
||||
bool flag = p.cmoOptionsOn[i];
|
||||
if (flag)
|
||||
{
|
||||
__instance.cmoOptions.Add((Player.CreativeOptions)i);
|
||||
}
|
||||
}**/
|
||||
|
||||
Main.helper.Log("Saving player upgrades");
|
||||
__instance.GetType().GetField("upgrades", bindingFlags).SetValue(__instance, new List<Player.UpgradeType>());
|
||||
|
||||
@@ -1745,9 +1736,10 @@ namespace KCM
|
||||
__instance.JobCustomMaxEnabledFlag = new bool[World.inst.NumLandMasses][];
|
||||
for (int lm = 0; lm < World.inst.NumLandMasses; lm++)
|
||||
{
|
||||
__instance.JobFilledAvailable[lm] = new int[38];
|
||||
__instance.JobCustomMaxEnabledFlag[lm] = new bool[38];
|
||||
for (int n = 0; n < 38; n++)
|
||||
int numJobTypes = p.JobFilledAvailable.data[lm].GetLength(0);
|
||||
__instance.JobFilledAvailable[lm] = new int[numJobTypes];
|
||||
__instance.JobCustomMaxEnabledFlag[lm] = new bool[numJobTypes];
|
||||
for (int n = 0; n < numJobTypes; n++)
|
||||
{
|
||||
__instance.JobFilledAvailable[lm][n] = p.JobFilledAvailable.data[lm][n, 1];
|
||||
}
|
||||
@@ -2257,19 +2249,7 @@ namespace KCM
|
||||
{
|
||||
Assembly assembly = typeof(Building).Assembly;
|
||||
|
||||
Type[] allTypes;
|
||||
try
|
||||
{
|
||||
allTypes = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
allTypes = e.Types.Where(t => t != null).ToArray();
|
||||
}
|
||||
|
||||
var types = allTypes
|
||||
.Where(t => t != null && typeof(Building).IsAssignableFrom(t) && !t.IsAbstract)
|
||||
.ToArray();
|
||||
Type[] types = new Type[] { typeof(Building) };
|
||||
|
||||
var methodsInNamespace = types
|
||||
.SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Where(m => !m.IsAbstract))
|
||||
@@ -2314,56 +2294,6 @@ namespace KCM
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch]
|
||||
public class FieldSystemPlayerReferencePatch
|
||||
{
|
||||
static FieldInfo playerField;
|
||||
|
||||
static IEnumerable<MethodBase> TargetMethods()
|
||||
{
|
||||
var methodsInNamespace = typeof(FieldSystem)
|
||||
.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||
.Where(m => !m.IsAbstract)
|
||||
.ToList();
|
||||
|
||||
helper.Log("Methods in namespace: " + methodsInNamespace.Count);
|
||||
|
||||
return methodsInNamespace.ToArray().Cast<MethodBase>();
|
||||
}
|
||||
|
||||
static IEnumerable<CodeInstruction> Transpiler(MethodBase method, IEnumerable<CodeInstruction> instructions)
|
||||
{
|
||||
if (playerField == null)
|
||||
{
|
||||
var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
|
||||
playerField = typeof(FieldSystem).GetFields(bindingFlags).FirstOrDefault(f => f.FieldType == typeof(Player));
|
||||
}
|
||||
|
||||
if (playerField == null)
|
||||
return instructions;
|
||||
|
||||
int playerInstCount = 0;
|
||||
|
||||
var codes = new List<CodeInstruction>(instructions);
|
||||
for (var i = 0; i < codes.Count; i++)
|
||||
{
|
||||
if (codes[i].opcode == OpCodes.Ldsfld && codes[i].operand.ToString() == "Player inst")
|
||||
{
|
||||
playerInstCount++;
|
||||
|
||||
codes[i].opcode = OpCodes.Ldarg_0;
|
||||
codes[i].operand = null;
|
||||
codes.Insert(++i, new CodeInstruction(OpCodes.Ldfld, playerField));
|
||||
}
|
||||
}
|
||||
|
||||
if (playerInstCount > 0)
|
||||
Main.helper.Log($"Found {playerInstCount} static FieldSystem Player.inst references in {method.Name}");
|
||||
|
||||
return codes.AsEnumerable();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[HarmonyPatch]
|
||||
public class PlayerPatch
|
||||
|
||||
91
Packets/Game/GameBuilding/BuildingRemovePacket.cs
Normal file
91
Packets/Game/GameBuilding/BuildingRemovePacket.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.GameBuilding
|
||||
{
|
||||
public class BuildingRemovePacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)KCM.Enums.Packets.BuildingRemove;
|
||||
|
||||
// Flag to prevent infinite loop when removing buildings from packet
|
||||
public static bool isProcessingPacket = false;
|
||||
|
||||
public Guid guid { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (clientId == KCClient.client.Id) return;
|
||||
|
||||
Main.helper.Log($"Received building remove packet for guid {guid} from {player.name}");
|
||||
|
||||
// Try to find the building in the player who owns it
|
||||
Building building = player.inst.GetBuilding(guid);
|
||||
|
||||
if (building == null)
|
||||
{
|
||||
// Try to find it in any player's buildings
|
||||
foreach (var kcp in Main.kCPlayers.Values)
|
||||
{
|
||||
building = kcp.inst.GetBuilding(guid);
|
||||
if (building != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (building == null)
|
||||
{
|
||||
Main.helper.Log($"Building with guid {guid} not found on client, may already be removed.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Main.helper.Log($"Removing building {building.UniqueName} at {building.transform.position}");
|
||||
|
||||
// Set flag to prevent sending packet back
|
||||
isProcessingPacket = true;
|
||||
|
||||
// Set Player.inst to the correct player for this building
|
||||
// This ensures the removal modifies the correct player's job lists
|
||||
Player originalPlayer = Player.inst;
|
||||
Player correctPlayer = Main.GetPlayerByBuilding(building);
|
||||
if (correctPlayer != null)
|
||||
{
|
||||
Player.inst = correctPlayer;
|
||||
}
|
||||
|
||||
// Use reflection to call the Remove method from the game assembly
|
||||
MethodInfo removeMethod = typeof(Building).GetMethod("Remove", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (removeMethod != null)
|
||||
{
|
||||
removeMethod.Invoke(building, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: destroy the building GameObject directly
|
||||
Main.helper.Log("Remove method not found, using Destroy fallback");
|
||||
building.destroyedWhileInPlay = true;
|
||||
UnityEngine.Object.Destroy(building.gameObject);
|
||||
}
|
||||
|
||||
// Restore original Player.inst
|
||||
Player.inst = originalPlayer;
|
||||
isProcessingPacket = false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
isProcessingPacket = false;
|
||||
Main.helper.Log($"Error removing building: {e.Message}");
|
||||
Main.helper.Log(e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
// Forward the remove packet to all other clients
|
||||
SendToAll(clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ namespace KCM.Packets.Handlers
|
||||
if (!KCServer.IsRunning)
|
||||
{
|
||||
Main.kCPlayers.Clear();
|
||||
Main.clientSteamIds.Clear();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -84,9 +84,7 @@ namespace KCM.Packets.Handlers
|
||||
{
|
||||
|
||||
IPacket p = (IPacket)Activator.CreateInstance(packet);
|
||||
var properties = packet.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Where(prop => prop.Name != "packetId" && prop.Name != "sendMode")
|
||||
.ToArray();
|
||||
var properties = packet.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(prop => prop.Name != "packetId").ToArray();
|
||||
Array.Sort(properties, (x, y) => String.Compare(x.Name, y.Name));
|
||||
ushort id = (ushort)p.GetType().GetProperty("packetId").GetValue(p, null);
|
||||
|
||||
@@ -224,20 +222,14 @@ namespace KCM.Packets.Handlers
|
||||
try
|
||||
{
|
||||
var packetRef = Packets[packet.packetId];
|
||||
|
||||
MessageSendMode sendMode = MessageSendMode.Reliable;
|
||||
Packet basePacket = packet as Packet;
|
||||
if (basePacket != null)
|
||||
sendMode = basePacket.sendMode;
|
||||
|
||||
Message message = Message.Create(sendMode, packet.packetId);
|
||||
Message message = Message.Create(MessageSendMode.Reliable, packet.packetId);
|
||||
|
||||
foreach (var prop in packetRef.properties)
|
||||
{
|
||||
if (prop.PropertyType.IsEnum)
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddInt(Convert.ToInt32(prop.GetValue(packet, null)));
|
||||
message.AddInt((int)prop.GetValue(packet, null));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(ushort))
|
||||
{
|
||||
@@ -469,7 +461,9 @@ namespace KCM.Packets.Handlers
|
||||
if (prop.PropertyType.IsEnum)
|
||||
{
|
||||
int enumValue = message.GetInt();
|
||||
prop.SetValue(p, Enum.ToObject(prop.PropertyType, enumValue));
|
||||
string enumName = Enum.GetName(prop.PropertyType, enumValue);
|
||||
|
||||
prop.SetValue(p, Enum.Parse(prop.PropertyType, enumName));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(ushort))
|
||||
{
|
||||
|
||||
@@ -35,17 +35,13 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
Main.helper.Log("PlayerList: " + playersName[i] + " " + playersId[i] + " " + steamIds[i]);
|
||||
|
||||
KCPlayer player;
|
||||
if (!Main.kCPlayers.TryGetValue(steamIds[i], out player) || player == null)
|
||||
Main.kCPlayers.Add(steamIds[i], new KCPlayer(playersName[i], playersId[i], steamIds[i])
|
||||
{
|
||||
player = new KCPlayer(playersName[i], playersId[i], steamIds[i]);
|
||||
Main.kCPlayers[steamIds[i]] = player;
|
||||
}
|
||||
|
||||
player.name = playersName[i];
|
||||
player.ready = playersReady[i];
|
||||
player.banner = playersBanner[i];
|
||||
player.kingdomName = playersKingdomName[i];
|
||||
name = playersName[i],
|
||||
ready = playersReady[i],
|
||||
banner = playersBanner[i],
|
||||
kingdomName = playersKingdomName[i]
|
||||
});
|
||||
|
||||
|
||||
if (Main.clientSteamIds.ContainsKey(playersId[i]))
|
||||
@@ -53,8 +49,7 @@ namespace KCM.Packets.Lobby
|
||||
else
|
||||
Main.clientSteamIds.Add(playersId[i], steamIds[i]);
|
||||
|
||||
if (player.inst != null && player.inst.PlayerLandmassOwner != null)
|
||||
player.inst.PlayerLandmassOwner.SetBannerIdx(playersBanner[i]);
|
||||
Main.kCPlayers[steamIds[i]].inst.PlayerLandmassOwner.SetBannerIdx(playersBanner[i]);
|
||||
|
||||
LobbyHandler.AddPlayerEntry(playersId[i]);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
if (player == null)
|
||||
return;
|
||||
IsReady = !player.ready;
|
||||
//SendToAll(KCClient.client.Id);
|
||||
|
||||
@@ -24,8 +22,6 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (player == null)
|
||||
return;
|
||||
player.ready = IsReady;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using KCM.StateManagement.Observers;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
using static KCM.Main;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
@@ -18,14 +18,6 @@ namespace KCM.Packets.Lobby
|
||||
public static bool loadingSave = false;
|
||||
public static int received = 0;
|
||||
|
||||
public static void ResetTransferState()
|
||||
{
|
||||
loadingSave = false;
|
||||
received = 0;
|
||||
saveData = new byte[1];
|
||||
chunksReceived = new bool[1];
|
||||
}
|
||||
|
||||
|
||||
public int chunkId { get; set; }
|
||||
public int chunkSize { get; set; }
|
||||
@@ -38,108 +30,90 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
bool initialisingTransfer = !loadingSave ||
|
||||
saveData == null ||
|
||||
saveData.Length != saveSize ||
|
||||
chunksReceived == null ||
|
||||
chunksReceived.Length != totalChunks;
|
||||
|
||||
if (initialisingTransfer)
|
||||
// Initialize on first chunk OR if arrays aren't properly sized yet
|
||||
// This handles out-of-order packet delivery
|
||||
if (!loadingSave || saveData.Length != saveSize || chunksReceived.Length != totalChunks)
|
||||
{
|
||||
Main.helper.Log("Save Transfer started!");
|
||||
Main.helper.Log($"Save Transfer initializing. saveSize={saveSize}, totalChunks={totalChunks}");
|
||||
loadingSave = true;
|
||||
received = 0;
|
||||
|
||||
StateObserver.ClearAll();
|
||||
|
||||
saveData = new byte[saveSize];
|
||||
chunksReceived = new bool[totalChunks];
|
||||
received = 0;
|
||||
|
||||
if (ServerLobbyScript.LoadingSave != null)
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
}
|
||||
|
||||
if (chunkId < 0 || chunkId >= totalChunks)
|
||||
// Skip if we already received this chunk (duplicate packet)
|
||||
if (chunksReceived[chunkId])
|
||||
{
|
||||
Main.helper.Log($"Invalid save chunk id: {chunkId} / {totalChunks}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveDataChunk == null)
|
||||
{
|
||||
Main.helper.Log($"Null save chunk data for chunk: {chunkId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveDataIndex < 0 || saveDataIndex + saveDataChunk.Length > saveData.Length)
|
||||
{
|
||||
Main.helper.Log($"Invalid save chunk write range: index={saveDataIndex} len={saveDataChunk.Length} size={saveData.Length}");
|
||||
Main.helper.Log($"[SaveTransfer] Duplicate chunk {chunkId} received, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
Array.Copy(saveDataChunk, 0, saveData, saveDataIndex, saveDataChunk.Length);
|
||||
|
||||
chunksReceived[chunkId] = true;
|
||||
|
||||
received += chunkSize;
|
||||
|
||||
float savePercent = saveSize > 0 ? (float)received / (float)saveSize : 0f;
|
||||
if (ServerLobbyScript.ProgressBar != null)
|
||||
ServerLobbyScript.ProgressBar.fillAmount = savePercent;
|
||||
if (ServerLobbyScript.ProgressBarText != null)
|
||||
ServerLobbyScript.ProgressBarText.text = (savePercent * 100).ToString("0.00") + "%";
|
||||
if (ServerLobbyScript.ProgressText != null)
|
||||
ServerLobbyScript.ProgressText.text = $"{((float)(received / 1000)).ToString("0.00")} KB / {((float)(saveSize / 1000)).ToString("0.00")} KB";
|
||||
Main.helper.Log($"[SaveTransfer] Processed chunk {chunkId}/{totalChunks}. Received: {received} bytes of {saveSize}.");
|
||||
|
||||
|
||||
if (chunkId + 1 == totalChunks)
|
||||
// Update progress bar
|
||||
if (saveSize > 0)
|
||||
{
|
||||
Main.helper.Log($"Received last save transfer packet.");
|
||||
float savePercent = (float)received / (float)saveSize;
|
||||
string receivedKB = ((float)received / 1000f).ToString("0.00");
|
||||
string totalKB = ((float)saveSize / 1000f).ToString("0.00");
|
||||
|
||||
Main.helper.Log(WhichIsNotComplete());
|
||||
ServerLobbyScript.ProgressBar.fillAmount = savePercent;
|
||||
ServerLobbyScript.ProgressBarText.text = (savePercent * 100).ToString("0.00") + "%";
|
||||
ServerLobbyScript.ProgressText.text = $"{receivedKB} KB / {totalKB} KB";
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerLobbyScript.ProgressBar.fillAmount = 0f;
|
||||
ServerLobbyScript.ProgressBarText.text = "0.00%";
|
||||
ServerLobbyScript.ProgressText.text = "0.00 KB / 0.00 KB";
|
||||
}
|
||||
|
||||
// Check if all chunks have been received
|
||||
if (IsTransferComplete())
|
||||
{
|
||||
// Handle completed transfer here
|
||||
Main.helper.Log("Save Transfer complete!");
|
||||
|
||||
// Reset the loading state before processing
|
||||
loadingSave = false;
|
||||
|
||||
LoadSaveLoadHook.saveBytes = saveData;
|
||||
LoadSaveLoadHook.memoryStreamHook = true;
|
||||
|
||||
LoadSave.Load();
|
||||
|
||||
GameState.inst.SetNewMode(GameState.inst.playingMode);
|
||||
LobbyManager.loadingSave = false;
|
||||
|
||||
LoadSaveLoadHook.saveContainer.Unpack(null);
|
||||
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
|
||||
|
||||
if (ServerLobbyScript.LoadingSave != null)
|
||||
ServerLobbyScript.LoadingSave.SetActive(false);
|
||||
ServerLobbyScript.LoadingSave.SetActive(false);
|
||||
|
||||
loadingSave = false;
|
||||
received = 0;
|
||||
saveData = new byte[1];
|
||||
chunksReceived = new bool[1];
|
||||
// Reset static state for next transfer
|
||||
ResetTransferState();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ResetTransferState()
|
||||
{
|
||||
saveData = new byte[1];
|
||||
chunksReceived = new bool[1];
|
||||
loadingSave = false;
|
||||
received = 0;
|
||||
}
|
||||
|
||||
public static bool IsTransferComplete()
|
||||
{
|
||||
return chunksReceived.All(x => x == true);
|
||||
}
|
||||
|
||||
public static string WhichIsNotComplete()
|
||||
{
|
||||
string notComplete = "";
|
||||
for (int i = 0; i < chunksReceived.Length; i++)
|
||||
{
|
||||
if (!chunksReceived[i])
|
||||
{
|
||||
notComplete += i + ", ";
|
||||
}
|
||||
}
|
||||
return notComplete;
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -18,38 +18,26 @@ namespace KCM.Packets.Lobby
|
||||
{
|
||||
Main.helper.Log(GameState.inst.mainMenuMode.ToString());
|
||||
|
||||
// Hide server lobby
|
||||
Main.TransitionTo((MenuState)200);
|
||||
|
||||
// This is run when user clicks "accept" on choose your map screeen
|
||||
|
||||
try
|
||||
{
|
||||
if (!LobbyManager.loadingSave)
|
||||
{
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
|
||||
try
|
||||
{
|
||||
typeof(MainMenuMode).GetMethod("StartGame", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(GameState.inst.mainMenuMode, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
}
|
||||
else
|
||||
try
|
||||
{
|
||||
LobbyManager.loadingSave = false;
|
||||
GameState.inst.SetNewMode(GameState.inst.playingMode);
|
||||
typeof(MainMenuMode).GetMethod("StartGame", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(GameState.inst.mainMenuMode, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exception here
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
@@ -57,47 +45,18 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
Start();
|
||||
if (!LobbyManager.loadingSave)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//Start();
|
||||
|
||||
|
||||
/*AIBrainsContainer.PreStartAIConfig aiConfig = new AIBrainsContainer.PreStartAIConfig();
|
||||
int count = 0;
|
||||
for (int i = 0; i < RivalKingdomSettingsUI.inst.rivalItems.Length; i++)
|
||||
{
|
||||
RivalItemUI r = RivalKingdomSettingsUI.inst.rivalItems[i];
|
||||
bool flag = r.Enabled && !r.Locked;
|
||||
if (flag)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
int idx = 0;
|
||||
aiConfig.startData = new AIBrainsContainer.PreStartAIConfig.AIStartData[count];
|
||||
for (int j = 0; j < RivalKingdomSettingsUI.inst.rivalItems.Length; j++)
|
||||
{
|
||||
RivalItemUI item = RivalKingdomSettingsUI.inst.rivalItems[j];
|
||||
bool flag2 = item.Enabled && !item.Locked;
|
||||
if (flag2)
|
||||
{
|
||||
aiConfig.startData[idx] = new AIBrainsContainer.PreStartAIConfig.AIStartData();
|
||||
aiConfig.startData[idx].landmass = item.flag.landmass;
|
||||
aiConfig.startData[idx].bioCode = item.bannerIdx;
|
||||
aiConfig.startData[idx].personalityKey = PersonalityCollection.aiPersonalityKeys[0];
|
||||
aiConfig.startData[idx].skillLevel = item.GetSkillLevel();
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
AIBrainsContainer.inst.aiStartInfo = aiConfig;
|
||||
bool isControllerActive = GamepadControl.inst.isControllerActive;
|
||||
if (isControllerActive)
|
||||
{
|
||||
ConsoleCursorMenu.inst.PrepForGamepad();
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,20 +51,6 @@ namespace KCM.Packets.Network
|
||||
{
|
||||
Main.helper.Log("Server Player Connected: " + Name + " Id: " + clientId + " SteamID: " + SteamId);
|
||||
|
||||
KCPlayer player;
|
||||
if (Main.kCPlayers.TryGetValue(SteamId, out player))
|
||||
{
|
||||
player.id = clientId;
|
||||
player.name = Name;
|
||||
player.steamId = SteamId;
|
||||
}
|
||||
else
|
||||
{
|
||||
Main.kCPlayers[SteamId] = new KCPlayer(Name, clientId, SteamId);
|
||||
}
|
||||
|
||||
Main.clientSteamIds[clientId] = SteamId;
|
||||
|
||||
List<KCPlayer> list = Main.kCPlayers.Select(x => x.Value).OrderBy(x => x.id).ToList();
|
||||
|
||||
if (list.Count > 0)
|
||||
@@ -92,7 +78,33 @@ namespace KCM.Packets.Network
|
||||
return;
|
||||
|
||||
byte[] bytes = LoadSaveLoadAtPathHook.saveData;
|
||||
KCServer.EnqueueSaveTransfer(clientId, bytes);
|
||||
int chunkSize = 900; // 900 bytes per chunk to fit within packet size limit
|
||||
|
||||
List<byte[]> chunks = SplitByteArrayIntoChunks(bytes, chunkSize);
|
||||
Main.helper.Log("Save Transfer started!");
|
||||
|
||||
int sent = 0;
|
||||
int packetsSent = 0;
|
||||
|
||||
for (int i = 0; i < chunks.Count; i++)
|
||||
{
|
||||
var chunk = chunks[i];
|
||||
|
||||
|
||||
new SaveTransferPacket()
|
||||
{
|
||||
saveSize = bytes.Length,
|
||||
saveDataChunk = chunk,
|
||||
chunkId = i,
|
||||
chunkSize = chunk.Length,
|
||||
saveDataIndex = sent,
|
||||
totalChunks = chunks.Count
|
||||
}.SendReliable(clientId);
|
||||
packetsSent++;
|
||||
sent += chunk.Length;
|
||||
}
|
||||
|
||||
Main.helper.Log($"Sent {packetsSent} save data chunks to client");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -38,8 +38,7 @@ namespace KCM.Packets.Network
|
||||
|
||||
Main.helper.Log("Sending client connected. Client ID is: " + clientId);
|
||||
|
||||
Main.kCPlayers[Main.PlayerSteamID] = new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID);
|
||||
Main.clientSteamIds[clientId] = Main.PlayerSteamID;
|
||||
Main.kCPlayers.Add(Main.PlayerSteamID, new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID));
|
||||
|
||||
Player.inst.PlayerLandmassOwner.teamId = clientId * 10 + 2;
|
||||
|
||||
|
||||
@@ -11,21 +11,22 @@ namespace KCM.Packets
|
||||
{
|
||||
public abstract ushort packetId { get; }
|
||||
public ushort clientId { get; set; }
|
||||
public virtual Riptide.MessageSendMode sendMode => Riptide.MessageSendMode.Reliable;
|
||||
|
||||
public KCPlayer player
|
||||
{
|
||||
get
|
||||
{
|
||||
string steamId;
|
||||
if (!Main.clientSteamIds.TryGetValue(clientId, out steamId) || string.IsNullOrEmpty(steamId))
|
||||
KCPlayer p = null;
|
||||
|
||||
if (!Main.clientSteamIds.ContainsKey(clientId))
|
||||
return null;
|
||||
if (Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(clientId).steamId, out p))
|
||||
return p;
|
||||
else
|
||||
{
|
||||
Main.helper.Log($"Error getting player from packet {packetId} {this.GetType().Name} from {clientId}");
|
||||
}
|
||||
|
||||
KCPlayer player;
|
||||
if (Main.kCPlayers.TryGetValue(steamId, out player))
|
||||
return player;
|
||||
|
||||
Main.helper.Log($"Error getting player from packet {packetId} {GetType().Name} from {clientId}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -103,6 +104,37 @@ namespace KCM.Packets
|
||||
}
|
||||
}
|
||||
|
||||
public void SendReliable(ushort toClient)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (KCServer.IsRunning && toClient != 0)
|
||||
{
|
||||
KCServer.server.Send(PacketHandler.SerialisePacket(this), toClient, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log($"Error sending reliable packet {packetId} {this.GetType().Name} from {clientId}");
|
||||
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void HandlePacketServer();
|
||||
public abstract void HandlePacketClient();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace KCM.Packets.State
|
||||
public class BuildingStatePacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.BuildingStatePacket;
|
||||
public override Riptide.MessageSendMode sendMode => Riptide.MessageSendMode.Unreliable;
|
||||
|
||||
public string customName { get; set; }
|
||||
public Guid guid { get; set; }
|
||||
|
||||
93
README.md
93
README.md
@@ -1,77 +1,38 @@
|
||||
# KCM (Kingdoms and Castles Multiplayer) – javított verzió
|
||||
# Kingdoms and Castles Multiplayer Mod Fixes
|
||||
|
||||
Ez a repo egy *Kingdoms and Castles* multiplayer mod forrását tartalmazza, pár stabilitási/szinkron hibára célzott javításokkal.
|
||||
This document summarizes the fixes and improvements implemented to enhance the stability and functionality of the multiplayer mod for Kingdoms and Castles.
|
||||
|
||||
## Mi volt a gond?
|
||||
## Implemented Fixes:
|
||||
|
||||
A mellékelt log (`output.txt`) alapján több tipikus hiba okozta a szerver indításkori/ lobby-beli szétesést:
|
||||
### 1. Improved Lobby Stability
|
||||
- **Issue:** Previously, joining a multiplayer lobby could lead to an immediate crash (NullReferenceException in `PlayerEntryScript.cs`).
|
||||
- **Fix:** Corrected the initialization order of UI components in `PlayerEntryScript.cs` to prevent NullReferenceExceptions, ensuring stable lobby entry.
|
||||
|
||||
- `NullReferenceException` a lobby player UI frissítésében (`PlayerEntryScript.SetValues`)
|
||||
- duplikált SteamID miatti `ArgumentException: same key already added` a handshake során
|
||||
- csomagkezelés közben `KeyNotFoundException` / `NullReferenceException` (hiányzó `clientId -> steamId` map, race/állapot problémák)
|
||||
- Save/load utani desync: a kliens oldali "Client Player" Reset nem futott mentés betöltése közben (loading flag miatt), így remote player állapot (registry/field/worker) beragadhatott; ez okozhatott "nem látni az aratást/elszállítást", hiányzó épület/resource state, és UI anomáliákat.
|
||||
- Save/load utani "kezdo keep" kerese: betöltéskor a keep beallitasa túl szigorúan teamId-hez volt kötve, ezért előfordult hogy a kliensnél nem lett beállítva a már létező keep, és újra kérte a játék a kezdő lerakást.
|
||||
- Többszöri load után romló sync: a statikus StateObserver/observer GameObject-ok nem lettek kitakarítva új load előtt, így régi világ/objektum referenciák maradhattak bent és rossz state frissítéseket küldhettek.
|
||||
### 2. Enhanced Session Cleanup
|
||||
- **Issue:** Users previously had to restart the entire game after leaving a multiplayer session to join or host a new one. This was due to residual game state and an aggressive cleanup that inadvertently shut down Steamworks.
|
||||
- **Fix:** Implemented a comprehensive `CleanupMultiplayerSession()` routine in `Main.cs`. This routine now properly resets static mod data (player lists, client/server states), and, crucially, no longer destroys the core `KCMSteamManager` (Steamworks API manager). This allows for seamless transitions between multiplayer sessions without game restarts.
|
||||
|
||||
## Mit javít ez a verzió?
|
||||
### 3. Optimized Building Synchronization Performance
|
||||
- **Issue:** Rapid changes in building state (e.g., during construction) could generate excessive network traffic, potentially contributing to "poor connection" issues.
|
||||
- **Fix:** Implemented a throttling mechanism in `BuildingStateManager.cs`. Building state updates are now limited to 10 times per second per building, significantly reducing network spam while maintaining visual fluidity.
|
||||
|
||||
- Lobby UI frissítés stabilizálása (null/állapot ellenőrzések, helyes inicializálási sorrend)
|
||||
- Handshake alatt a player-regisztráció ütközésmentessé tétele + `clientSteamIds` beállítása
|
||||
- Packet oldali player lookup biztonságossá tétele (ne dobjon kivételt hiányzó map esetén)
|
||||
- `PlayerReady` packet: ha nincs player, ne crasheljen
|
||||
- Szerver oldalon a csatlakozáskor a játékos regisztráció/map frissítése
|
||||
- Kilépés/clear esetén `clientSteamIds` takarítása, hogy ne maradjanak “árva” bejegyzések
|
||||
- Épületek `Player.inst` referenciáinak patch-elése már nem csak a base `Building` osztályban fut, hanem az összes `Building`-ből származó típusban (pl. farmok speciális logikája)
|
||||
- `FieldSystem` `Player.inst` referenciáinak patch-elése (farm/termés állapotkezelés több helyen erre támaszkodik)
|
||||
- Mentés betöltéskor a `ProcessBuilding` útvonal kiegészítése `World.inst.PlaceFromLoad(...)` + `UnpackStage2(...)` hívásokkal (különösen fontos a “világba helyezés” mellékhatásai miatt, pl. farm/field regisztráció)
|
||||
- Save transfer kliens oldalon robusztusabb inicializálás/reset (ne ragadjon be a statikus állapot több betöltés után, plusz bounds/null ellenőrzések)
|
||||
- Fix: save/load közben is lefut a remote "Client Player" Reset (nem csak új világ generálásnál), hogy a player alrendszerek mindig tiszta alapból induljanak.
|
||||
- Fix: keep detektálás betöltéskor (ne teamId egyezésen múljon), így nem kéri a játék a kezdő keep lerakását, ha már létezik.
|
||||
- Fix: új load előtt StateObserver takarítás (save transfer kezdetén, host oldali `LoadAtPath` elején, lobby elhagyásakor), hogy ne maradjanak beragadt observer objektumok.
|
||||
- Kompatibilitási fix: `World.inst.liverySets` lista esetén `.Count` használata `.Length` helyett (különben `Compilation failed` lehet egyes verziókon)
|
||||
- Hálózati stabilitás: `BuildingStatePacket` most `Unreliable` módban megy (state jellegű csomagoknál jobb, ha a legfrissebb állapot érkezik meg és nem torlódik fel a megbízható sor)
|
||||
- Mentés-szinkron stabilitás: szerver oldalon a save chunkok már nem egy nagy for-ciklusban mennek ki, hanem ütemezve (csökkenti a “The gap between received sequence IDs…” / “Poor connection” diszkonnekteket)
|
||||
- Kapcsolat tuning: kliens és szerver oldalon emelt `MaxSendAttempts`, és tiltott minőség-alapú auto-disconnect (különösen save transfer közben volt agresszív)
|
||||
- Fix: a `sendMode` csak a Riptide üzenetküldési mód kiválasztására szolgál, nem kerül szériázásra; az enum csomagmezők szériázása/deszériázása robusztusabb lett (különben csatlakozáskor packet-parszolás szétesett)
|
||||
### 4. Resolved Villager Freezing
|
||||
- **Issue:** Villagers would sometimes freeze during gameplay, despite other game elements functioning correctly. This was caused by the game attempting to synchronize the state of already destroyed building components, leading to a cascade of errors.
|
||||
- **Fix:** Added a robust null check in `BuildingStateManager.cs`. If an observed building has been destroyed, its associated observer is now properly de-registered (by destroying its GameObject), preventing further errors and ensuring continuous game logic for villagers and other entities. This also handles cases where buildings are replaced (e.g., construction completed).
|
||||
|
||||
Érintett fájlok (főbb pontok):
|
||||
### 5. Fixed Map Desynchronization
|
||||
- **Issue:** When starting a new multiplayer game, clients often generated a different map than the host, even if the seed was specified. This was due to the host not sending the definitive world seed at the critical moment.
|
||||
- **Fix:** Modified `ServerLobbyScript.cs` to ensure that when the host clicks "Start Game", the current world seed (either from UI input or newly generated) is explicitly sent to all clients via a `WorldSeed` packet *before* the game starts. This guarantees all players generate the exact same map.
|
||||
|
||||
- `ServerLobby/PlayerEntryScript.cs`
|
||||
- `Packets/Network/ServerHandshake.cs`
|
||||
- `Packets/Network/ClientConnected.cs`
|
||||
- `Packets/Packet.cs`
|
||||
- `Packets/Lobby/PlayerReady.cs`
|
||||
- `Packets/Lobby/PlayerList.cs`
|
||||
- `Packets/Lobby/SaveTransferPacket.cs`
|
||||
- `KCServer.cs`
|
||||
- `Packets/Handlers/LobbyHandler.cs`
|
||||
- `RiptideSteamTransport/LobbyManager.cs`
|
||||
- `Packets/Handlers/PacketHandler.cs`
|
||||
- `Packets/State/BuildingStatePacket.cs`
|
||||
- `StateManagement/Observers/StateObserver.cs`
|
||||
- `Main.cs`
|
||||
### 6. Reliable Save Game Transfer
|
||||
- **Issue:** Loading a saved multiplayer game would often fail for clients, resulting in an incomplete save file and desynchronized gameplay. This occurred because save file chunks were sent unreliably over the network.
|
||||
- **Fix:** Changed the save game chunk transfer in `ClientConnected.cs` to use Riptide's `Reliable` message send mode. This ensures that all parts of the save file are guaranteed to arrive at the client, allowing for complete and successful save game loading.
|
||||
|
||||
## Telepítés / használat
|
||||
### 7. Compilation Errors & Warnings Addressed
|
||||
- All reported compilation errors and warnings (including issues with `Packet.Send` overloads and `World.SeedFromText`) have been investigated and resolved, ensuring the mod compiles cleanly.
|
||||
|
||||
Fontos: a hostnak és **minden kliensnek ugyanaz a verzió** kell, különben továbbra is lehetnek sync problémák.
|
||||
## Pending Task:
|
||||
|
||||
Megjegyzés: a mod menüben a piros `Restart to load` üzenet azt jelenti, hogy a mod engedélyezése/letöltése közben változott valami, és **teljes játék-újraindítás** kell, hogy betöltődjön.
|
||||
|
||||
1. Tedd a mod mappáját a játék `mods` könyvtárába (vagy használd Workshopból, de ott egy frissítés felülírhatja a javításokat).
|
||||
2. Indítsd újra teljesen a játékot.
|
||||
3. Hostolj/ csatlakozz, majd ellenőrizd, hogy a lobby és a szerver indítás stabil marad.
|
||||
|
||||
Workshop módosításokhoz ajánlott: másold ki a Workshop mappából egy **külön névvel** a `...\\KingdomsAndCastles_Data\\mods\\` alá, és a mod menüben kapcsold ki a Workshop verziót, hogy Steam frissítés ne írja felül.
|
||||
|
||||
## Hibaelhárítás
|
||||
|
||||
Ha továbbra is hibát látsz:
|
||||
|
||||
- Küldd el a `output.txt` releváns részét (a hiba előtti/utáni stack trace-t), vagy írd le a pontos üzenetet.
|
||||
- Írd meg, hogy: hostoltál-e, hány kliens csatlakozott, és mindenkin ugyanaz a mod-verzió van-e.
|
||||
- Teszthez kapcsold ki a többi modot (különösen azokat, amik Harmony patch-elnek). A logban egy `Profiler` mod (`Profiler.ProfilerMod`) is hibázott, ez meg tudja zavarni a betöltést.
|
||||
- Farm/termés desync esetén írd meg: host vagy kliens oldalon nem látszik-e a termés, új világban történik-e vagy save betöltés után, és hány perc játék után jön elő.
|
||||
|
||||
## Repo higiénia
|
||||
|
||||
- A `.gitignore` kizárja a logokat (`output*.txt`) és tipikus IDE/build artifactokat, hogy ne kerüljenek fel GitHubra.
|
||||
### Resource Synchronization
|
||||
- **Goal:** Implement synchronization for player resources (Gold, Wood, Stone, Food) to ensure all players see consistent resource counts.
|
||||
- **Status:** Awaiting confirmation from the user regarding the exact `FreeResourceType` enum names (`Wood`, `Stone`, `Food`) to proceed with implementation.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using KCM;
|
||||
using KCM.Enums;
|
||||
using KCM.Packets.Handlers;
|
||||
using KCM.StateManagement.Observers;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -154,7 +153,20 @@ namespace Riptide.Demos.Steam.PlayerHosted
|
||||
//NetworkManager.Singleton.StopServer();
|
||||
//NetworkManager.Singleton.DisconnectClient();
|
||||
SteamMatchmaking.LeaveLobby(lobbyId);
|
||||
Main.ResetMultiplayerState("LeaveLobby");
|
||||
|
||||
if (KCClient.client.IsConnected)
|
||||
KCClient.client.Disconnect();
|
||||
|
||||
Main.helper.Log("clear players");
|
||||
Main.kCPlayers.Clear();
|
||||
LobbyHandler.ClearPlayerList();
|
||||
LobbyHandler.ClearChatEntries();
|
||||
Main.helper.Log("end clear players");
|
||||
|
||||
if (KCServer.IsRunning)
|
||||
KCServer.server.Stop();
|
||||
|
||||
|
||||
|
||||
Main.TransitionTo(MenuState.ServerBrowser);
|
||||
ServerBrowser.registerServer = false;
|
||||
|
||||
@@ -299,13 +299,35 @@ namespace KCM
|
||||
|
||||
try
|
||||
{
|
||||
GameObject kcmUICanvas = Instantiate(Constants.MainMenuUI_T.Find("TopLevelUICanvas").gameObject);
|
||||
if (Constants.MainMenuUI_T == null)
|
||||
{
|
||||
Main.helper.Log("MainMenuUI_T is null in ServerBrowser");
|
||||
return;
|
||||
}
|
||||
|
||||
var topLevelCanvas = ResolveMenuCanvas();
|
||||
if (topLevelCanvas == null)
|
||||
{
|
||||
Main.helper.Log("Failed to resolve top-level menu canvas in ServerBrowser");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject kcmUICanvas = Instantiate(topLevelCanvas.gameObject);
|
||||
|
||||
for (int i = 0; i < kcmUICanvas.transform.childCount; i++)
|
||||
Destroy(kcmUICanvas.transform.GetChild(i).gameObject);
|
||||
|
||||
kcmUICanvas.name = "KCMUICanvas";
|
||||
kcmUICanvas.transform.SetParent(Constants.MainMenuUI_T);
|
||||
kcmUICanvas.transform.SetParent(Constants.MainMenuUI_T, false);
|
||||
kcmUICanvas.transform.SetAsLastSibling();
|
||||
kcmUICanvas.SetActive(false);
|
||||
|
||||
var canvasComponent = kcmUICanvas.GetComponent<Canvas>();
|
||||
if (canvasComponent != null)
|
||||
{
|
||||
canvasComponent.overrideSorting = true;
|
||||
canvasComponent.sortingOrder = 999;
|
||||
}
|
||||
|
||||
KCMUICanvas = kcmUICanvas.transform;
|
||||
|
||||
@@ -322,6 +344,8 @@ namespace KCM
|
||||
serverLobbyPlayerRef = serverLobbyRef.transform.Find("Container/PlayerList/Viewport/Content");
|
||||
serverLobbyChatRef = serverLobbyRef.transform.Find("Container/PlayerChat/Viewport/Content");
|
||||
serverLobbyRef.SetActive(false);
|
||||
serverBrowserRef.transform.SetAsLastSibling();
|
||||
serverLobbyRef.transform.SetAsLastSibling();
|
||||
//browser.transform.position = new Vector3(0, 0, 0);
|
||||
|
||||
|
||||
@@ -435,6 +459,29 @@ namespace KCM
|
||||
}
|
||||
}
|
||||
|
||||
private Transform ResolveMenuCanvas()
|
||||
{
|
||||
string[] candidatePaths =
|
||||
{
|
||||
"TopLevelUICanvas",
|
||||
"TopLevel",
|
||||
"MainMenu/TopLevel/TopLevelUICanvas",
|
||||
"MainMenu/TopLevel"
|
||||
};
|
||||
|
||||
foreach (var path in candidatePaths)
|
||||
{
|
||||
var transform = Constants.MainMenuUI_T.Find(path);
|
||||
if (transform != null)
|
||||
{
|
||||
Main.helper.Log($"ServerBrowser: using canvas path '{path}'.");
|
||||
return transform;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
{
|
||||
helper.Log("Hello?");
|
||||
|
||||
@@ -51,24 +51,12 @@ namespace KCM.ServerLobby.LobbyChat
|
||||
{
|
||||
try
|
||||
{
|
||||
if (banner == null)
|
||||
return;
|
||||
|
||||
string steamId;
|
||||
if (!Main.clientSteamIds.TryGetValue(Client, out steamId))
|
||||
return;
|
||||
|
||||
KCPlayer player;
|
||||
if (!Main.kCPlayers.TryGetValue(steamId, out player) || player == null)
|
||||
return;
|
||||
Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(Client).steamId, out player);
|
||||
|
||||
if (World.inst == null || World.inst.liverySets == null)
|
||||
return;
|
||||
|
||||
if (player.banner < 0 || player.banner >= World.inst.liverySets.Count)
|
||||
return;
|
||||
|
||||
banner.texture = World.inst.liverySets[player.banner].banners;
|
||||
var bannerTexture = World.inst.liverySets[player.banner].banners;
|
||||
|
||||
banner.texture = bannerTexture;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace KCM.ServerLobby
|
||||
|
||||
transform.Find("PlayerBanner").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
Main.TransitionTo(MenuState.NameAndBanner);
|
||||
Main.TransitionTo(MenuState.NameAndBanner);//ChooseBannerUI Hooks required, as well as townnameui
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,39 +37,21 @@ namespace KCM.ServerLobby
|
||||
{
|
||||
try
|
||||
{
|
||||
if (banner == null)
|
||||
// First check if the client still exists
|
||||
if (!Main.TryGetPlayerByClientID(Client, out KCPlayer player) || player == null)
|
||||
{
|
||||
var bannerTransform = transform.Find("PlayerBanner");
|
||||
if (bannerTransform == null)
|
||||
return;
|
||||
banner = bannerTransform.GetComponent<RawImage>();
|
||||
if (banner == null)
|
||||
return;
|
||||
// Client no longer exists, stop the repeating invoke and destroy this entry
|
||||
CancelInvoke("SetValues");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
string steamId;
|
||||
if (!Main.clientSteamIds.TryGetValue(Client, out steamId))
|
||||
return;
|
||||
transform.Find("PlayerName").GetComponent<TextMeshProUGUI>().text = player.name;
|
||||
transform.Find("Ready").gameObject.SetActive(player.ready);
|
||||
|
||||
KCPlayer player;
|
||||
if (!Main.kCPlayers.TryGetValue(steamId, out player) || player == null)
|
||||
return;
|
||||
var bannerTexture = World.inst.liverySets[player.banner].banners;
|
||||
|
||||
var nameTransform = transform.Find("PlayerName");
|
||||
if (nameTransform != null)
|
||||
nameTransform.GetComponent<TextMeshProUGUI>().text = player.name ?? "";
|
||||
|
||||
var readyTransform = transform.Find("Ready");
|
||||
if (readyTransform != null)
|
||||
readyTransform.gameObject.SetActive(player.ready);
|
||||
|
||||
if (World.inst == null || World.inst.liverySets == null)
|
||||
return;
|
||||
|
||||
if (player.banner < 0 || player.banner >= World.inst.liverySets.Count)
|
||||
return;
|
||||
|
||||
banner.texture = World.inst.liverySets[player.banner].banners;
|
||||
banner.texture = bannerTexture;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -142,7 +142,6 @@ namespace KCM
|
||||
{
|
||||
|
||||
Main.helper.Log("Disable all");
|
||||
//StartGameButton.gameObject.SetActive(false);
|
||||
StartGameButton.onClick.RemoveAllListeners();
|
||||
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Ready";
|
||||
StartGameButton.onClick.AddListener(() =>
|
||||
@@ -187,6 +186,32 @@ namespace KCM
|
||||
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Start";
|
||||
StartGameButton.onClick.AddListener(() =>
|
||||
{
|
||||
int definitiveSeed;
|
||||
if (string.IsNullOrWhiteSpace(WorldSeed.text))
|
||||
{
|
||||
World.inst.Generate();
|
||||
definitiveSeed = World.inst.seed;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (int.TryParse(WorldSeed.text, out int parsedSeed))
|
||||
{
|
||||
definitiveSeed = parsedSeed;
|
||||
World.inst.Generate(definitiveSeed);
|
||||
}
|
||||
else
|
||||
{
|
||||
Main.helper.Log($"Invalid seed '{WorldSeed.text}' entered. Generating a random seed.");
|
||||
World.inst.Generate();
|
||||
definitiveSeed = World.inst.seed;
|
||||
}
|
||||
}
|
||||
|
||||
new WorldSeed()
|
||||
{
|
||||
Seed = definitiveSeed
|
||||
}.SendToAll(KCClient.client.Id);
|
||||
|
||||
new StartGame().SendToAll();
|
||||
|
||||
if (PlacementType.value == 0 && !LobbyManager.loadingSave)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using KCM.Packets;
|
||||
using KCM.Packets;
|
||||
using KCM.Packets.State;
|
||||
using KCM.StateManagement.Observers;
|
||||
using System;
|
||||
@@ -6,12 +6,15 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using static KCM.StateManagement.Observers.Observer;
|
||||
|
||||
namespace KCM.StateManagement.BuildingState
|
||||
{
|
||||
public class BuildingStateManager
|
||||
{
|
||||
private static readonly Dictionary<Guid, float> lastUpdateTime = new Dictionary<Guid, float>();
|
||||
private const float UpdateInterval = 0.1f; // 10 times per second
|
||||
|
||||
public static void BuildingStateChanged(object sender, StateUpdateEventArgs args)
|
||||
{
|
||||
@@ -23,9 +26,29 @@ namespace KCM.StateManagement.BuildingState
|
||||
try
|
||||
{
|
||||
Observer observer = (Observer)sender;
|
||||
|
||||
Building building = (Building)observer.state;
|
||||
|
||||
if (building == null)
|
||||
{
|
||||
if(observer != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(observer.gameObject);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Guid guid = building.guid;
|
||||
|
||||
if (lastUpdateTime.ContainsKey(guid) && Time.time < lastUpdateTime[guid] + UpdateInterval)
|
||||
{
|
||||
return; // Not time to update yet
|
||||
}
|
||||
|
||||
if (!lastUpdateTime.ContainsKey(guid))
|
||||
lastUpdateTime.Add(guid, Time.time);
|
||||
else
|
||||
lastUpdateTime[guid] = Time.time;
|
||||
|
||||
//Main.helper.Log("Should send building network update for: " + building.UniqueName);
|
||||
|
||||
new BuildingStatePacket()
|
||||
@@ -57,4 +80,4 @@ namespace KCM.StateManagement.BuildingState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,24 +12,6 @@ namespace KCM.StateManagement.Observers
|
||||
{
|
||||
public static Dictionary<int, IObserver> observers = new Dictionary<int, IObserver>();
|
||||
|
||||
public static void ClearAll()
|
||||
{
|
||||
foreach (var observer in observers.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
var component = observer as Component;
|
||||
if (component != null)
|
||||
UnityEngine.Object.Destroy(component.gameObject);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
observers.Clear();
|
||||
}
|
||||
|
||||
public static void RegisterObserver<T>(T instance, string[] monitoredFields, EventHandler<StateUpdateEventArgs> eventHandler = null, EventHandler<StateUpdateEventArgs> sendUpdateHandler = null)
|
||||
{
|
||||
if (observers.ContainsKey(instance.GetHashCode()))
|
||||
|
||||
@@ -15,6 +15,11 @@ namespace KCM.UI
|
||||
class KaC_Button
|
||||
{
|
||||
public Button Button = null;
|
||||
private static readonly string[] ButtonPaths =
|
||||
{
|
||||
"TopLevelUICanvas/TopLevel/Body/ButtonContainer/New",
|
||||
"MainMenu/TopLevel/Body/ButtonContainer/New" // fallback for older versions
|
||||
};
|
||||
|
||||
public string Name
|
||||
{
|
||||
@@ -84,14 +89,18 @@ namespace KCM.UI
|
||||
set => Transform.SetSiblingIndex(value);
|
||||
}
|
||||
|
||||
public KaC_Button(Transform parent = null)
|
||||
{
|
||||
Button b = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").GetComponent<Button>();
|
||||
public KaC_Button(Transform parent = null) : this(null, parent) { }
|
||||
|
||||
if (parent == null)
|
||||
Button = GameObject.Instantiate(b);
|
||||
else
|
||||
Button = GameObject.Instantiate(b, parent);
|
||||
public KaC_Button(Button b, Transform parent = null)
|
||||
{
|
||||
var templateButton = ResolveTemplateButton(b);
|
||||
|
||||
if (templateButton == null)
|
||||
throw new InvalidOperationException("Template button not found in main menu UI.");
|
||||
|
||||
Button = parent == null
|
||||
? GameObject.Instantiate(templateButton)
|
||||
: GameObject.Instantiate(templateButton, parent);
|
||||
|
||||
foreach (Localize Localize in Button.GetComponentsInChildren<Localize>())
|
||||
GameObject.Destroy(Localize);
|
||||
@@ -99,20 +108,27 @@ namespace KCM.UI
|
||||
Button.onClick = new Button.ButtonClickedEvent();
|
||||
}
|
||||
|
||||
public KaC_Button(Button b, Transform parent = null)
|
||||
private static Button ResolveTemplateButton(Button providedButton)
|
||||
{
|
||||
if (b == null)
|
||||
b = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").GetComponent<Button>();
|
||||
if (providedButton != null)
|
||||
return providedButton;
|
||||
|
||||
if (parent == null)
|
||||
Button = GameObject.Instantiate(b);
|
||||
else
|
||||
Button = GameObject.Instantiate(b, parent);
|
||||
foreach (var path in ButtonPaths)
|
||||
{
|
||||
var transform = Constants.MainMenuUI_T?.Find(path);
|
||||
if (transform == null)
|
||||
continue;
|
||||
|
||||
foreach (Localize Localize in Button.GetComponentsInChildren<Localize>())
|
||||
GameObject.Destroy(Localize);
|
||||
var button = transform.GetComponent<Button>();
|
||||
if (button != null)
|
||||
{
|
||||
Main.helper?.Log($"Using menu button template at '{path}'.");
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
Button.onClick = new Button.ButtonClickedEvent();
|
||||
Main.helper?.Log("Failed to find menu button template for KaC_Button.");
|
||||
return null;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
Reference in New Issue
Block a user