Compare commits

..

28 Commits

Author SHA1 Message Date
8b9c19186b fix: Update button container paths for consistency and improve fallback handling 2025-12-15 16:49:21 +01:00
99398b5479 fix: Improve logging for menu transitions and resolve top-level canvas references in ServerBrowser 2025-12-15 10:39:38 +01:00
270a92c617 fix: Update button template resolution and improve error logging for missing components 2025-12-15 09:56:55 +01:00
4d8279719c fix: Update button container path to reflect correct hierarchy and improve logging 2025-12-15 09:51:02 +01:00
3d5a53f0e2 fix: Enhance button container pathfinding and add null checks for ServerBrowser references 2025-12-15 09:49:37 +01:00
25f5af0b4d fix: Add null checks for MainMenuUI_T and TopLevelUICanvas in ServerBrowser to prevent instantiation errors 2025-12-15 09:40:11 +01:00
2ad605138e fix: Implement lazy initialization for Constants to prevent null reference errors 2025-12-15 09:36:27 +01:00
ca517be369 refactor: Remove BuildingRemoveHook to streamline building removal process 2025-12-15 09:35:39 +01:00
df1def69e4 fix: Restore correct namespace reference for packetId in BuildingRemovePacket 2025-12-15 09:32:00 +01:00
db850885f6 fix: Correct packetId reference in BuildingRemovePacket to use the appropriate Enums 2025-12-15 09:30:25 +01:00
71e1e09c75 feat: Enhance building removal logic to ensure correct player job list modifications and add fallback for Remove method 2025-12-15 09:27:05 +01:00
46ebeb1f80 feat: Enhance BuildingRemove logic to correctly manage Player.inst during building removal 2025-12-15 09:22:50 +01:00
7d06145a34 feat: Implement BuildingRemoveHook to manage building removal and prevent infinite loops
fix: Enhance SaveTransferPacket handling for out-of-order delivery and reset transfer state
2025-12-15 09:19:46 +01:00
fcf1ffac76 feat: Add BuildingRemovePacket to handle building removal requests in-game 2025-12-15 09:19:42 +01:00
40369ffe4b fix: Improve player retrieval logic and handle non-existent clients in PlayerEntryScript 2025-12-15 09:15:31 +01:00
fc089afcc0 fix: Enhance logging for save transfer process and completion checks in SaveTransferPacket 2025-12-15 00:06:36 +01:00
cb82d3706f fix: Add missing using directive for Riptide.Demos.Steam.PlayerHosted in SaveTransferPacket 2025-12-14 23:58:51 +01:00
12a207989e fix: Reset save transfer state and streamline loading logic in SaveTransferPacket and StartGame 2025-12-14 23:56:41 +01:00
4afcaccf75 fix: Reset save transfer state and update progress bar calculations 2025-12-14 23:54:49 +01:00
8f13282e04 fix: Improve job type initialization and handle game state during loading 2025-12-14 23:45:12 +01:00
0d7d989f76 fix: Change log warning to info for invalid seed handling in world generation 2025-12-14 23:24:57 +01:00
1cc3042781 feat: Implement various multiplayer stability and synchronization fixes
This commit addresses several critical issues reported by the user to improve the stability and synchronization of the Kingdoms and Castles multiplayer mod.

Key changes include:

- Improved Lobby Stability: Fixed NullReferenceException during lobby entry.
- Enhanced Session Cleanup: Refined disconnection logic to prevent Steamworks shutdown and enable seamless new game starts without client restarts.
- Optimized Building Synchronization: Implemented a throttling mechanism for building state updates to reduce network traffic.
- Resolved Villager Freezing: Introduced a null check for destroyed observed buildings to prevent synchronization cascades.
- Fixed Map Desynchronization: Ensured the host reliably sends the world seed to all clients before game start.
- Reliable Save Game Transfer: Switched save file chunk transfer to reliable messaging mode to prevent incomplete save loads.
- Addressed Compilation Issues: Resolved all compilation errors and warnings that arose from the implemented fixes.
2025-12-14 23:22:57 +01:00
181936e3d4 Refactor seed handling in game start logic and improve packet sending reliability 2025-12-14 23:22:34 +01:00
62db70c1c4 Refactor packet sending to use SendReliable method for improved reliability and remove unnecessary logging 2025-12-14 23:18:23 +01:00
36acbb57c5 Enhance reliability of chunk data transmission and update world seed handling on game start 2025-12-14 23:13:43 +01:00
76f1033bd2 Fix null reference handling in building state updates 2025-12-14 23:00:23 +01:00
26b5f1201e Refactor player entry initialization and optimize building state update logic 2025-12-14 22:27:43 +01:00
9ee675ac19 Implement multiplayer session cleanup on client disconnection 2025-12-14 22:12:50 +01:00
26 changed files with 619 additions and 583 deletions

View File

@@ -6,8 +6,7 @@
"Bash(ls:*)", "Bash(ls:*)",
"Bash(git add:*)", "Bash(git add:*)",
"Bash(git commit:*)", "Bash(git commit:*)",
"Bash(tail:*)", "Bash(tail:*)"
"Bash(connection\", or \"server disconnected\" messages, and couldn''t reconnect \nwithout restarting the game. Game state wasn''t properly cleaned up \nafter disconnect.\n\nRoot causes:\n1. Static client/server objects never reinitialized after disconnect\n2. Event handlers lost when new client/server instances created\n3. Incomplete state cleanup after disconnect\n4. Short timeout values (5s) causing frequent disconnections\n\nSolutions:\n\nKCClient.cs:\n- Add InitializeClient() method that:\n * Cleans up old client instance\n * Disconnects existing connections\n * Unsubscribes from old event handlers\n * Creates fresh Client instance\n * Sets higher timeout (15s -> reduces timeouts by ~70%)\n * Re-subscribes to all event handlers\n- Connect() now reinitializes client before each connection attempt\n- Increased max connection attempts (5 -> 10)\n- Improved Client_Disconnected handler:\n * Clears clientSteamIds state\n * Distinguishes voluntary vs unexpected disconnects\n * Only shows error modal for unexpected disconnects\n\nKCServer.cs:\n- Add InitializeServer() method with same cleanup pattern\n- Extract event handlers to static methods (OnClientConnected, \n OnClientDisconnected) so they persist across server instances\n- StartServer() now reinitializes server for clean state\n- Add try-catch in OnClientDisconnected to prevent crashes\n- Set higher timeout (15s) to reduce disconnections\n\nLobbyManager.cs:\n- Complete rewrite of LeaveLobby() with:\n * Detailed logging for debugging\n * Null-safe checks for all operations\n * Try-catch wrapper for safe cleanup\n * Clears both kCPlayers and clientSteamIds\n * Resets all flags (loadingSave, registerServer)\n * Guarantees return to ServerBrowser even on errors\n\nResults:\n✅ Players can now reconnect without restarting game\n✅ ~70% reduction in timeout/poor connection messages\n✅ Clean state after every disconnect\n✅ Event handlers remain stable across reinitializations\n✅ Better error handling and logging for diagnostics\n\nAdded comprehensive README.md documenting:\n- All fixes with code examples\n- Previous fixes (map sync, StartGame NullRef)\n- Installation and usage instructions\n- Known issues section (currently none)\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>\nEOF\n)\")"
] ]
} }
} }

View File

@@ -15,19 +15,20 @@ namespace KCM
/// </summary> /// </summary>
public static class Constants public static class Constants
{ {
public static readonly MainMenuMode MainMenuMode = GameState.inst.mainMenuMode; // Use lazy initialization to avoid null reference when GameState isn't ready yet
public static readonly PlayingMode PlayingMode = GameState.inst.playingMode; public static MainMenuMode MainMenuMode => GameState.inst?.mainMenuMode;
public static readonly World World = GameState.inst.world; public static PlayingMode PlayingMode => GameState.inst?.playingMode;
public static World World => GameState.inst?.world;
#region "UI" #region "UI"
public static readonly Transform MainMenuUI_T = MainMenuMode.mainMenuUI.transform; public static Transform MainMenuUI_T => MainMenuMode?.mainMenuUI?.transform;
public static readonly GameObject MainMenuUI_O = MainMenuMode.mainMenuUI; public static GameObject MainMenuUI_O => MainMenuMode?.mainMenuUI;
/* public static readonly Transform TopLevelUI_T = MainMenuUI_T.parent; /* public static readonly Transform TopLevelUI_T = MainMenuUI_T.parent;
public static readonly GameObject TopLevelUI_O = MainMenuUI_T.parent.gameObject;*/ public static readonly GameObject TopLevelUI_O = MainMenuUI_T.parent.gameObject;*/
public static readonly Transform ChooseModeUI_T = MainMenuMode.chooseModeUI.transform; public static Transform ChooseModeUI_T => MainMenuMode?.chooseModeUI?.transform;
public static readonly GameObject ChooseModeUI_O = MainMenuMode.chooseModeUI; public static GameObject ChooseModeUI_O => MainMenuMode?.chooseModeUI;
#endregion #endregion
} }

View File

@@ -18,11 +18,8 @@ namespace KCM.Enums
KingdomName = 32, KingdomName = 32,
StartGame = 33, StartGame = 33,
WorldSeed = 34, WorldSeed = 34,
Building = 50, Building = 50,
BuildingOnPlacement = 51, BuildingOnPlacement = 51,
World = 70, World = 70,
WorldPlace = 71, WorldPlace = 71,
FellTree = 72, FellTree = 72,
@@ -44,6 +41,7 @@ namespace KCM.Enums
AddVillager = 88, AddVillager = 88,
SetupInitialWorkers = 89, SetupInitialWorkers = 89,
VillagerTeleportTo = 90, VillagerTeleportTo = 90,
PlaceKeepRandomly = 91 PlaceKeepRandomly = 91,
BuildingRemove = 92
} }
} }

View File

@@ -19,7 +19,7 @@ namespace KCM
{ {
public class KCClient : MonoBehaviour public class KCClient : MonoBehaviour
{ {
public static Client client; public static Client client = new Client(Main.steamClient);
public string Name { get; set; } public string Name { get; set; }
@@ -28,33 +28,6 @@ namespace KCM
static KCClient() static KCClient()
{ {
InitializeClient();
}
private static void InitializeClient()
{
// Clean up old client if exists
if (client != null)
{
if (client.IsConnected || client.IsConnecting)
{
client.Disconnect();
}
// Unsubscribe from old events
client.Connected -= Client_Connected;
client.ConnectionFailed -= Client_ConnectionFailed;
client.Disconnected -= Client_Disconnected;
client.MessageReceived -= PacketHandler.HandlePacket;
}
// Create new client instance
client = new Client(Main.steamClient);
// Set a higher timeout to prevent frequent disconnections
client.TimeoutTime = 15000; // 15 seconds instead of default 5 seconds
// Subscribe to events
client.Connected += Client_Connected; client.Connected += Client_Connected;
client.ConnectionFailed += Client_ConnectionFailed; client.ConnectionFailed += Client_ConnectionFailed;
client.Disconnected += Client_Disconnected; client.Disconnected += Client_Disconnected;
@@ -63,44 +36,34 @@ namespace KCM
private static void Client_Disconnected(object sender, DisconnectedEventArgs e) private static void Client_Disconnected(object sender, DisconnectedEventArgs e)
{ {
Main.helper.Log($"Client disconnected event start - Reason: {e.Reason}"); Main.CleanupMultiplayerSession();
Main.helper.Log("Client disconnected event start");
try try
{ {
// Clean up client state
Main.clientSteamIds.Clear();
// Only show modal if disconnect was unexpected (not voluntary)
bool wasVoluntary = e.Reason == DisconnectReason.Disconnected;
if (e.Message != null) if (e.Message != null)
{ {
Main.helper.Log("Processing disconnect message..."); Main.helper.Log(e.Message.ToString());
MessageReceivedEventArgs eargs = new MessageReceivedEventArgs(null, (ushort)Enums.Packets.ShowModal, e.Message); MessageReceivedEventArgs eargs = new MessageReceivedEventArgs(null, (ushort)Enums.Packets.ShowModal, e.Message);
if (eargs.MessageId == (ushort)Enums.Packets.ShowModal) if (eargs.MessageId == (ushort)Enums.Packets.ShowModal)
{ {
ShowModal modalPacket = (ShowModal)PacketHandler.DeserialisePacket(eargs); ShowModal modalPacket = (ShowModal)PacketHandler.DeserialisePacket(eargs);
modalPacket.HandlePacketClient(); modalPacket.HandlePacketClient();
} }
} }
else if (!wasVoluntary)
{
// Only show error modal for unexpected disconnections
Main.helper.Log("Showing disconnect modal to user");
GameState.inst.SetNewMode(GameState.inst.mainMenuMode);
ModalManager.ShowModal("Disconnected from Server", ErrorCodeMessages.GetMessage(e.Reason), "Okay", true, () => { Main.TransitionTo(MenuState.ServerBrowser); });
}
else else
{ {
Main.helper.Log("Voluntary disconnect - no modal shown");
GameState.inst.SetNewMode(GameState.inst.mainMenuMode);
ModalManager.ShowModal("Disconnected from Server", ErrorCodeMessages.GetMessage(e.Reason), "Okay", true, () => { Main.TransitionTo(MenuState.ServerBrowser); });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Main.helper.Log("Error handling disconnection message:"); Main.helper.Log("Error handling disconnection message");
Main.helper.Log(ex.Message); Main.helper.Log(ex.ToString());
Main.helper.Log(ex.StackTrace);
} }
Main.helper.Log("Client disconnected event end"); Main.helper.Log("Client disconnected event end");
} }
@@ -126,11 +89,7 @@ namespace KCM
public static void Connect(string ip) public static void Connect(string ip)
{ {
Main.helper.Log("Trying to connect to: " + ip); Main.helper.Log("Trying to connect to: " + ip);
client.Connect(ip, useMessageHandlers: false);
// Reinitialize client to ensure clean state before connecting
InitializeClient();
client.Connect(ip, maxConnectionAttempts: 10, useMessageHandlers: false);
} }
private void Update() private void Update()

View File

@@ -18,44 +18,24 @@ namespace KCM
{ {
public class KCServer : MonoBehaviour public class KCServer : MonoBehaviour
{ {
public static Server server; public static Server server = new Server(Main.steamServer);
public static bool started = false; public static bool started = false;
static KCServer() static KCServer()
{ {
// Initialize server in static constructor //server.registerMessageHandler(typeof(KCServer).GetMethod("ClientJoined"));
InitializeServer();
}
private static void InitializeServer()
{
// Clean up old server if exists
if (server != null)
{
if (server.IsRunning)
{
server.Stop();
}
// Unsubscribe from old events
server.MessageReceived -= PacketHandler.HandlePacketServer;
server.ClientConnected -= OnClientConnected;
server.ClientDisconnected -= OnClientDisconnected;
}
// Create new server instance
server = new Server(Main.steamServer);
// Set a higher timeout to prevent frequent disconnections
server.TimeoutTime = 15000; // 15 seconds instead of default 5 seconds
// Subscribe to events
server.MessageReceived += PacketHandler.HandlePacketServer; server.MessageReceived += PacketHandler.HandlePacketServer;
server.ClientConnected += OnClientConnected;
server.ClientDisconnected += OnClientDisconnected;
} }
private static void OnClientConnected(object obj, ServerConnectedEventArgs ev) public static void StartServer()
{
server = new Server(Main.steamServer);
server.MessageReceived += PacketHandler.HandlePacketServer;
server.Start(0, 25, useMessageHandlers: false);
server.ClientConnected += (obj, ev) =>
{ {
Main.helper.Log("Client connected"); Main.helper.Log("Client connected");
@@ -65,7 +45,7 @@ namespace KCM
showModal.Send(ev.Client.Id); showModal.Send(ev.Client.Id);
server.DisconnectClient(ev.Client.Id); server.DisconnectClient(ev.Client.Id); //, PacketHandler.SerialisePacket(showModal)
return; return;
} }
@@ -74,13 +54,9 @@ namespace KCM
Main.helper.Log("Client ID is: " + ev.Client.Id); Main.helper.Log("Client ID is: " + ev.Client.Id);
new ServerHandshake() { clientId = ev.Client.Id, loadingSave = LobbyManager.loadingSave }.Send(ev.Client.Id); new ServerHandshake() { clientId = ev.Client.Id, loadingSave = LobbyManager.loadingSave }.Send(ev.Client.Id);
} };
private static void OnClientDisconnected(object obj, ServerDisconnectedEventArgs ev) server.ClientDisconnected += (obj, ev) =>
{
try
{
if (Main.clientSteamIds.ContainsKey(ev.Client.Id))
{ {
new ChatSystemMessage() new ChatSystemMessage()
{ {
@@ -88,34 +64,37 @@ namespace KCM
}.SendToAll(); }.SendToAll();
Main.kCPlayers.Remove(Main.GetPlayerByClientID(ev.Client.Id).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);
var playerEntry = LobbyHandler.playerEntries
.Select(x => x.GetComponent<PlayerEntryScript>())
.Where(x => x.Client == ev.Client.Id)
.FirstOrDefault();
if (playerEntry != null)
Destroy(playerEntry.gameObject);
}
Main.helper.Log($"Client disconnected. {ev.Reason}"); Main.helper.Log($"Client disconnected. {ev.Reason}");
} };
catch (Exception ex)
{
Main.helper.Log($"Error handling client disconnect: {ex.Message}");
}
}
public static void StartServer()
{
// Reinitialize server to ensure clean state
InitializeServer();
server.Start(0, 25, useMessageHandlers: false);
Main.helper.Log($"Listening on port 7777. Max {LobbyHandler.ServerSettings.MaxPlayers} clients."); Main.helper.Log($"Listening on port 7777. Max {LobbyHandler.ServerSettings.MaxPlayers} clients.");
//Main.kCPlayers.Add(1, new KCPlayer(1, Player.inst));
//Player.inst = Main.GetPlayer();
} }
/*[MessageHandler(25)]
public static void ClientJoined(ushort id, Message message)
{
var name = message.GetString();
Main.helper.Log(id.ToString());
Main.helper.Log($"User connected: {name}");
if (id == 1)
{
players.Add(id, new KCPlayer(name, id, Player.inst));
}
else
{
players.Add(id, new KCPlayer(name, id));
}
}*/
public static bool IsRunning { get { return server.IsRunning; } } public static bool IsRunning { get { return server.IsRunning; } }
private void Update() private void Update()

156
Main.cs
View File

@@ -57,7 +57,21 @@ namespace KCM
public static KCPlayer GetPlayerByClientID(ushort clientId) 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 public static Player GetPlayerByTeamID(int teamId) // Need to replace building / production types so that the correct player is used. IResourceStorage and IResourceProvider, and jobs
@@ -106,11 +120,51 @@ namespace KCM
public static string PlayerSteamID = SteamUser.GetSteamID().ToString(); public static string PlayerSteamID = SteamUser.GetSteamID().ToString();
public static KCMSteamManager KCMSteamManager = null; public static KCMSteamManager KCMSteamManager = null;
public static LobbyManager lobbyManager = null;
public static SteamServer steamServer = new SteamServer(); public static SteamServer steamServer = new SteamServer();
public static Riptide.Transports.Steam.SteamClient steamClient = new Riptide.Transports.Steam.SteamClient(steamServer); public static Riptide.Transports.Steam.SteamClient steamClient = new Riptide.Transports.Steam.SteamClient(steamServer);
public static ushort currentClient = 0; 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" #region "SceneLoaded"
private void SceneLoaded(KCModHelper helper) private void SceneLoaded(KCModHelper helper)
{ {
@@ -123,14 +177,9 @@ namespace KCM
KCMSteamManager = new GameObject("KCMSteamManager").AddComponent<KCMSteamManager>(); KCMSteamManager = new GameObject("KCMSteamManager").AddComponent<KCMSteamManager>();
DontDestroyOnLoad(KCMSteamManager); DontDestroyOnLoad(KCMSteamManager);
var lobbyManager = new GameObject("LobbyManager").AddComponent<LobbyManager>(); lobbyManager = new GameObject("LobbyManager").AddComponent<LobbyManager>();
DontDestroyOnLoad(lobbyManager); DontDestroyOnLoad(lobbyManager);
//SteamFriends.InviteUserToGame(new CSteamID(76561198036307537), "test");
//SteamMatchmaking.lobby
//Main.helper.Log($"Timer duration for hazardpay {Player.inst.hazardPayWarmup.Duration}");
try try
{ {
@@ -140,24 +189,62 @@ namespace KCM
Main.helper.Log(JsonConvert.SerializeObject(World.inst.mapSizeDefs, Formatting.Indented)); 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", Name = "Multiplayer",
Text = "Multiplayer", Text = "Multiplayer",
FirstSibling = true, FirstSibling = true,
OnClick = () => OnClick = () =>
{ {
//Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel").gameObject.SetActive(false); Main.helper?.Log("Multiplayer button clicked");
SfxSystem.PlayUiSelect(); SfxSystem.PlayUiSelect();
//ServerBrowser.serverBrowserRef.SetActive(true);
TransitionTo(MenuState.ServerBrowser); TransitionTo(MenuState.ServerBrowser);
} }
}; };
serverBrowser.Transform.SetSiblingIndex(2); serverBrowser.Transform.SetSiblingIndex(2);
var kingdomShare = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/Kingdom Share")
Destroy(Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/Kingdom Share").gameObject); ?? Constants.MainMenuUI_T.Find("MainMenu/TopLevel/Body/ButtonContainer/Kingdom Share");
if (kingdomShare != null)
{
Destroy(kingdomShare.gameObject);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -224,11 +311,22 @@ namespace KCM
{ {
try try
{ {
// Null checks for ServerBrowser references
if (ServerBrowser.serverBrowserRef != null)
ServerBrowser.serverBrowserRef.SetActive(state == MenuState.ServerBrowser); ServerBrowser.serverBrowserRef.SetActive(state == MenuState.ServerBrowser);
if (ServerBrowser.serverLobbyRef != null)
ServerBrowser.serverLobbyRef.SetActive(state == MenuState.ServerLobby); ServerBrowser.serverLobbyRef.SetActive(state == MenuState.ServerLobby);
if (ServerBrowser.KCMUICanvas != null)
{
ServerBrowser.KCMUICanvas.gameObject.SetActive((int)state > 21); 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()); helper.Log(((int)state > 21).ToString());
}
GameState.inst.mainMenuMode.TransitionTo((MainMenuMode.State)state); GameState.inst.mainMenuMode.TransitionTo((MainMenuMode.State)state);
} }
@@ -258,9 +356,6 @@ namespace KCM
helper.Log("Preload start in main"); helper.Log("Preload start in main");
try try
{ {
//MainMenuPatches.Patch();
Main.helper = helper; Main.helper = helper;
helper.Log(helper.modPath); helper.Log(helper.modPath);
@@ -395,7 +490,6 @@ namespace KCM
// Your code here // Your code here
// Get the name of the last method that called OnPlayerPlacement // Get the name of the last method that called OnPlayerPlacement
string callTree = "";
List<string> strings = new List<string>(); List<string> strings = new List<string>();
for (int i = 1; i < 10; i++) for (int i = 1; i < 10; i++)
@@ -1255,6 +1349,8 @@ namespace KCM
{ {
Main.helper.Log("Attempting to load save from server"); Main.helper.Log("Attempting to load save from server");
try
{
using (MemoryStream ms = new MemoryStream(saveBytes)) using (MemoryStream ms = new MemoryStream(saveBytes))
{ {
BinaryFormatter bf = new BinaryFormatter(); BinaryFormatter bf = new BinaryFormatter();
@@ -1262,7 +1358,26 @@ namespace KCM
saveContainer = (MultiplayerSaveContainer)bf.Deserialize(ms); 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; return false;
} }
@@ -1621,9 +1736,10 @@ namespace KCM
__instance.JobCustomMaxEnabledFlag = new bool[World.inst.NumLandMasses][]; __instance.JobCustomMaxEnabledFlag = new bool[World.inst.NumLandMasses][];
for (int lm = 0; lm < World.inst.NumLandMasses; lm++) for (int lm = 0; lm < World.inst.NumLandMasses; lm++)
{ {
__instance.JobFilledAvailable[lm] = new int[38]; int numJobTypes = p.JobFilledAvailable.data[lm].GetLength(0);
__instance.JobCustomMaxEnabledFlag[lm] = new bool[38]; __instance.JobFilledAvailable[lm] = new int[numJobTypes];
for (int n = 0; n < 38; n++) __instance.JobCustomMaxEnabledFlag[lm] = new bool[numJobTypes];
for (int n = 0; n < numJobTypes; n++)
{ {
__instance.JobFilledAvailable[lm][n] = p.JobFilledAvailable.data[lm][n, 1]; __instance.JobFilledAvailable[lm][n] = p.JobFilledAvailable.data[lm][n, 1];
} }

View 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);
}
}
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Riptide.Demos.Steam.PlayerHosted;
using static KCM.Main; using static KCM.Main;
namespace KCM.Packets.Lobby namespace KCM.Packets.Lobby
@@ -29,84 +30,90 @@ namespace KCM.Packets.Lobby
public override void HandlePacketClient() public override void HandlePacketClient()
{ {
float savePercent = (float)received / (float)saveSize; // Initialize on first chunk OR if arrays aren't properly sized yet
// This handles out-of-order packet delivery
// Initialize saveData and chunksReceived on the first packet received if (!loadingSave || saveData.Length != saveSize || chunksReceived.Length != totalChunks)
if (saveData.Length == 1)
{ {
Main.helper.Log($"Save Transfer initializing. saveSize={saveSize}, totalChunks={totalChunks}");
Main.helper.Log("Save Transfer started!");
loadingSave = true; loadingSave = true;
ServerLobbyScript.LoadingSave.SetActive(true);
// save percentage
saveData = new byte[saveSize]; saveData = new byte[saveSize];
chunksReceived = new bool[totalChunks]; chunksReceived = new bool[totalChunks];
received = 0;
ServerLobbyScript.LoadingSave.SetActive(true);
} }
// Skip if we already received this chunk (duplicate packet)
if (chunksReceived[chunkId])
{
Main.helper.Log($"[SaveTransfer] Duplicate chunk {chunkId} received, skipping.");
return;
}
// Copy the chunk data into the correct position in saveData
Array.Copy(saveDataChunk, 0, saveData, saveDataIndex, saveDataChunk.Length); Array.Copy(saveDataChunk, 0, saveData, saveDataIndex, saveDataChunk.Length);
// Mark this chunk as received
chunksReceived[chunkId] = true; chunksReceived[chunkId] = true;
// Seek to the next position to write to
received += chunkSize; received += chunkSize;
Main.helper.Log($"[SaveTransfer] Processed chunk {chunkId}/{totalChunks}. Received: {received} bytes of {saveSize}.");
// Update progress bar
if (saveSize > 0)
{
float savePercent = (float)received / (float)saveSize;
string receivedKB = ((float)received / 1000f).ToString("0.00");
string totalKB = ((float)saveSize / 1000f).ToString("0.00");
ServerLobbyScript.ProgressBar.fillAmount = savePercent; ServerLobbyScript.ProgressBar.fillAmount = savePercent;
ServerLobbyScript.ProgressBarText.text = (savePercent * 100).ToString("0.00") + "%"; ServerLobbyScript.ProgressBarText.text = (savePercent * 100).ToString("0.00") + "%";
ServerLobbyScript.ProgressText.text = $"{((float)(received / 1000)).ToString("0.00")} KB / {((float)(saveSize / 1000)).ToString("0.00")} KB"; ServerLobbyScript.ProgressText.text = $"{receivedKB} KB / {totalKB} KB";
}
else
if (chunkId + 1 == totalChunks)
{ {
Main.helper.Log($"Received last save transfer packet."); ServerLobbyScript.ProgressBar.fillAmount = 0f;
ServerLobbyScript.ProgressBarText.text = "0.00%";
Main.helper.Log(WhichIsNotComplete()); ServerLobbyScript.ProgressText.text = "0.00 KB / 0.00 KB";
} }
// Check if all chunks have been received
if (IsTransferComplete()) if (IsTransferComplete())
{ {
// Handle completed transfer here
Main.helper.Log("Save Transfer complete!"); Main.helper.Log("Save Transfer complete!");
// Reset the loading state before processing
loadingSave = false;
LoadSaveLoadHook.saveBytes = saveData; LoadSaveLoadHook.saveBytes = saveData;
LoadSaveLoadHook.memoryStreamHook = true; LoadSaveLoadHook.memoryStreamHook = true;
LoadSave.Load(); LoadSave.Load();
GameState.inst.SetNewMode(GameState.inst.playingMode);
LobbyManager.loadingSave = false;
LoadSaveLoadHook.saveContainer.Unpack(null);
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent()); Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
ServerLobbyScript.LoadingSave.SetActive(false); ServerLobbyScript.LoadingSave.SetActive(false);
// 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() public static bool IsTransferComplete()
{ {
return chunksReceived.All(x => x == true); 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() public override void HandlePacketServer()
{ {
} }

View File

@@ -18,14 +18,9 @@ namespace KCM.Packets.Lobby
{ {
Main.helper.Log(GameState.inst.mainMenuMode.ToString()); Main.helper.Log(GameState.inst.mainMenuMode.ToString());
// Hide server lobby
Main.TransitionTo((MenuState)200); Main.TransitionTo((MenuState)200);
// This is run when user clicks "accept" on choose your map screeen
try try
{
if (!LobbyManager.loadingSave)
{ {
SpeedControlUI.inst.SetSpeed(0); SpeedControlUI.inst.SetSpeed(0);
@@ -41,63 +36,27 @@ namespace KCM.Packets.Lobby
SpeedControlUI.inst.SetSpeed(0); SpeedControlUI.inst.SetSpeed(0);
} }
else
{
LobbyManager.loadingSave = false;
GameState.inst.SetNewMode(GameState.inst.playingMode);
}
}
catch (Exception ex) catch (Exception ex)
{ {
// Handle exception here
Main.helper.Log(ex.Message.ToString()); Main.helper.Log(ex.Message.ToString());
Main.helper.Log(ex.ToString()); Main.helper.Log(ex.ToString());
} }
} }
public override void HandlePacketClient() public override void HandlePacketClient()
{
if (!LobbyManager.loadingSave)
{ {
Start(); Start();
} }
else
{
ServerLobbyScript.LoadingSave.SetActive(true);
}
}
public override void HandlePacketServer() 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();
}*/
} }
} }
} }

View File

@@ -99,10 +99,7 @@ namespace KCM.Packets.Network
chunkSize = chunk.Length, chunkSize = chunk.Length,
saveDataIndex = sent, saveDataIndex = sent,
totalChunks = chunks.Count totalChunks = chunks.Count
}.Send(clientId); }.SendReliable(clientId);
Main.helper.Log(" ");
packetsSent++; packetsSent++;
sent += chunk.Length; sent += chunk.Length;
} }

View File

@@ -20,9 +20,6 @@ namespace KCM.Packets
if (!Main.clientSteamIds.ContainsKey(clientId)) if (!Main.clientSteamIds.ContainsKey(clientId))
return null; return null;
//Main.helper.Log($"SteamID: {Main.GetPlayerByClientID(clientId).steamId} for {clientId} ({Main.GetPlayerByClientID(clientId).id})");
if (Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(clientId).steamId, out p)) if (Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(clientId).steamId, out p))
return p; return p;
else else
@@ -107,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 HandlePacketServer();
public abstract void HandlePacketClient(); public abstract void HandlePacketClient();
} }

254
README.md
View File

@@ -1,238 +1,38 @@
# Kingdoms and Castles Multiplayer Mod # Kingdoms and Castles Multiplayer Mod Fixes
Ez a mod multiplayer funkcionalitást ad a Kingdoms and Castles játékhoz. This document summarizes the fixes and improvements implemented to enhance the stability and functionality of the multiplayer mod for Kingdoms and Castles.
## Legutóbbi javítások (2025-12-14) ## Implemented Fixes:
### Kapcsolati problémák javítása ### 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.
#### Probléma ### 2. Enhanced Session Cleanup
A játékosok gyakran tapasztaltak "poor connection", "lost connection", vagy "server disconnected" üzeneteket, és ezután nem tudtak újra csatlakozni anélkül, hogy újraindították volna a játékot. - **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.
#### Gyökérok ### 3. Optimized Building Synchronization Performance
1. **Statikus client/server objektumok**: A `KCClient` és `KCServer` osztályok statikus konstruktorokban létrehozott `Client` és `Server` objektumokat használtak, amelyek soha nem újrainizializálódtak disconnect után. - **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.
2. **Event handler elvesztés**: Az event handlerek (Connected, Disconnected, MessageReceived, stb.) a statikus konstruktorban lettek feliratkozva, de amikor új server/client objektumok lettek létrehozva, ezek a handlerek elvesztek. ### 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).
3. **Nem tisztított állapot**: A disconnect után a játék állapota nem lett teljesen kitisztítva, így a következő kapcsolódási kísérlet hibás állapotból indult. ### 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.
#### Megoldások ### 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.
##### 1. KCClient.cs - Javított újracsatlakozási képesség ### 7. Compilation Errors & Warnings Addressed
**Változtatások:** - 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.
- Új `InitializeClient()` metódus amely:
- Teljesen kitisztítja a régi client objektumot
- Lecsatlakoztatja a régi kapcsolatot ha létezik
- Leiratkozik az összes event handlerről
- Létrehoz egy új, tiszta `Client` objektumot
- Növelt timeout (15 másodperc) a gyakori timeout-ok elkerülésére
- Újra feliratkozik az event handlerekre
- Módosított `Connect()` metódus: ## Pending Task:
- Minden kapcsolódás előtt újrainicializálja a client-et
- Növelt connection attempts (10) a megbízhatóbb kapcsolódásért
- Javított `Client_Disconnected` event handler: ### Resource Synchronization
- Tisztítja a client state-et (clientSteamIds) - **Goal:** Implement synchronization for player resources (Gold, Wood, Stone, Food) to ensure all players see consistent resource counts.
- Megkülönbözteti az önkéntes és nem várt disconnect-eket - **Status:** Awaiting confirmation from the user regarding the exact `FreeResourceType` enum names (`Wood`, `Stone`, `Food`) to proceed with implementation.
- Csak nem várt disconnect esetén mutat error modalt
**Érintett kód részletek:**
```csharp
// KCClient.cs:34-62
private static void InitializeClient()
{
// Clean up old client if exists
if (client != null)
{
if (client.IsConnected || client.IsConnecting)
{
client.Disconnect();
}
// Unsubscribe from old events
client.Connected -= Client_Connected;
client.ConnectionFailed -= Client_ConnectionFailed;
client.Disconnected -= Client_Disconnected;
client.MessageReceived -= PacketHandler.HandlePacket;
}
// Create new client instance
client = new Client(Main.steamClient);
// Set a higher timeout to prevent frequent disconnections
client.TimeoutTime = 15000; // 15 seconds instead of default 5 seconds
// Subscribe to events
client.Connected += Client_Connected;
client.ConnectionFailed += Client_ConnectionFailed;
client.Disconnected += Client_Disconnected;
client.MessageReceived += PacketHandler.HandlePacket;
}
```
##### 2. KCServer.cs - Javított server event handling
**Változtatások:**
- Új `InitializeServer()` metódus hasonló logikával mint a client:
- Leállítja a régi server-t ha fut
- Leiratkozik az event handlerökről
- Létrehoz új server objektumot
- Növelt timeout (15 másodperc)
- Újra feliratkozik az event handlerekre
- Event handlerek kiszervezése statikus metódusokba:
- `OnClientConnected()` - külön metódus a client csatlakozás kezelésére
- `OnClientDisconnected()` - külön metódus a client lecsatlakozás kezelésére
- Try-catch blokkok a biztonságos error handling-ért
- `StartServer()` mindig újrainicializálja a server-t:
- Garantálja a tiszta állapotot minden indításkor
**Érintett kód részletek:**
```csharp
// KCServer.cs:30-56
private static void InitializeServer()
{
// Clean up old server if exists
if (server != null)
{
if (server.IsRunning)
{
server.Stop();
}
// Unsubscribe from old events
server.MessageReceived -= PacketHandler.HandlePacketServer;
server.ClientConnected -= OnClientConnected;
server.ClientDisconnected -= OnClientDisconnected;
}
// Create new server instance
server = new Server(Main.steamServer);
// Set a higher timeout to prevent frequent disconnections
server.TimeoutTime = 15000; // 15 seconds instead of default 5 seconds
// Subscribe to events
server.MessageReceived += PacketHandler.HandlePacketServer;
server.ClientConnected += OnClientConnected;
server.ClientDisconnected += OnClientDisconnected;
}
```
##### 3. LobbyManager.cs - Javított cleanup
**Változtatások:**
- `LeaveLobby()` metódus teljes átírása:
- Részletes logging minden lépésnél
- Null-safe ellenőrzések
- Try-catch blokk a biztonságos cleanup-ért
- Kitisztítja mind a `kCPlayers` mind a `clientSteamIds` dictionary-ket
- Visszaállítja az összes flag-et (`loadingSave`, `registerServer`)
- Garantálja hogy mindig visszatér a ServerBrowser-hez még hiba esetén is
**Érintett kód részletek:**
```csharp
// LobbyManager.cs:151-205
public void LeaveLobby()
{
Main.helper.Log("LeaveLobby called - cleaning up connection state");
try
{
// Disconnect client first
if (KCClient.client != null && (KCClient.client.IsConnected || KCClient.client.IsConnecting))
{
Main.helper.Log("Disconnecting client...");
KCClient.client.Disconnect();
}
// Stop server if running
if (KCServer.IsRunning)
{
Main.helper.Log("Stopping server...");
KCServer.server.Stop();
}
// Leave Steam lobby
if (lobbyId.IsValid())
{
Main.helper.Log("Leaving Steam lobby...");
SteamMatchmaking.LeaveLobby(lobbyId);
}
// Clear player data
Main.helper.Log("Clearing player data...");
Main.kCPlayers.Clear();
Main.clientSteamIds.Clear();
// Clear UI
LobbyHandler.ClearPlayerList();
LobbyHandler.ClearChatEntries();
// Reset flags
ServerBrowser.registerServer = false;
loadingSave = false;
// ... continues with transition
}
catch (Exception ex)
{
// Error handling with fallback
}
}
```
### Eredmények
Ezekkel a változtatásokkal a következő problémák lettek megoldva:
1.**Reconnect képesség**: A játékosok most már tudnak újra csatlakozni disconnect után anélkül, hogy újraindítanák a játékot
2.**Timeout problémák csökkentése**: A 15 másodperces timeout jelentősen csökkenti a "poor connection" üzeneteket
3.**Tiszta állapot**: Minden disconnect után teljesen kitisztított állapotból indul az új kapcsolódás
4.**Event handler stabilitás**: Az event handlerek most már nem vesznek el újrainicializálás során
5.**Jobb error handling**: Try-catch blokkok és részletes logging a problémák könnyebb diagnosztizálásához
### Korábbi javítások
#### Map szinkronizáció javítás (c4eb7e9)
- Javítva: A client-ek rossz map paraméterekkel generáltak világot
- Megoldás: WorldSeed packet most tartalmazza az összes szükséges map paramétert (size, type, rivers)
#### StartGame NullReferenceException javítás (fc467f4)
- Javítva: NullReferenceException a multiplayer játék indításakor
- Megoldás: Eltávolítva a MainMenuMode.StartGame() reflection hívás, közvetlen átmenet playing mode-ba
## Telepítés
1. Másold a mod fájljait a Kingdoms and Castles mod mappájába
2. Aktiváld a modot a játék mod menüjében
3. Indítsd újra a játékot
## Használat
### Server létrehozása
1. Főmenü -> Multiplayer
2. Create Server
3. Állítsd be a szerver beállításokat
4. Várd meg, hogy a játékosok csatlakozzanak
### Csatlakozás serverhez
1. Főmenü -> Multiplayer
2. Válaszd ki a servert a listából
3. Kattints a "Join" gombra
## Ismert problémák
Jelenleg nincsenek ismert kritikus problémák. Ha hibát találsz, kérlek jelentsd a fejlesztőknek.
## Fejlesztői megjegyzések
### Debugging
A mod részletes loggolást tartalmaz, amely segít a problémák diagnosztizálásában. A logok a következő helyeken jelennek meg:
- `[WORLD SYNC]` prefix: World generation szinkronizációs események
- `LeaveLobby called` - `Lobby cleanup completed`: Disconnect/cleanup folyamat
### Hozzájárulás
Ha szeretnél hozzájárulni a mod fejlesztéséhez, pull request-eket szívesen fogadunk!
## Licensz
Ez a mod a Riptide Networking library-t használja, amely MIT licensz alatt áll.

View File

@@ -66,11 +66,16 @@ namespace Riptide.Demos.Steam.PlayerHosted
if (callback.m_eResult != EResult.k_EResultOK) if (callback.m_eResult != EResult.k_EResultOK)
{ {
//UIManager.Singleton.LobbyCreationFailed();
Main.helper.Log("Create lobby failed"); Main.helper.Log("Create lobby failed");
return; return;
} }
lobbyId = new CSteamID(callback.m_ulSteamIDLobby); lobbyId = new CSteamID(callback.m_ulSteamIDLobby);
//UIManager.Singleton.LobbyCreationSucceeded(callback.m_ulSteamIDLobby);
//NetworkManager.Singleton.Server.Start(0, 5, NetworkManager.PlayerHostedDemoMessageHandlerGroupId);
KCServer.StartServer(); KCServer.StartServer();
@@ -87,6 +92,16 @@ namespace Riptide.Demos.Steam.PlayerHosted
LobbyHandler.ClearPlayerList(); LobbyHandler.ClearPlayerList();
/*Cam.inst.desiredDist = 80f;
Cam.inst.desiredPhi = 45f;
CloudSystem.inst.threshold1 = 0.6f;
CloudSystem.inst.threshold2 = 0.8f;
CloudSystem.inst.BaseFreq = 4.5f;
Weather.inst.SetSeason(Weather.Season.Summer);
Main.TransitionTo(MenuState.NameAndBanner);*/
ServerBrowser.registerServer = true; ServerBrowser.registerServer = true;
} }
catch (System.Exception ex) catch (System.Exception ex)
@@ -107,6 +122,8 @@ namespace Riptide.Demos.Steam.PlayerHosted
Main.helper.Log(ex.InnerException.StackTrace); Main.helper.Log(ex.InnerException.StackTrace);
} }
} }
//NetworkManager.Singleton.Client.Connect("127.0.0.1", messageHandlerGroupId: NetworkManager.PlayerHostedDemoMessageHandlerGroupId);
} }
internal void JoinLobby(ulong lobbyId) internal void JoinLobby(ulong lobbyId)
@@ -128,62 +145,31 @@ namespace Riptide.Demos.Steam.PlayerHosted
CSteamID hostId = SteamMatchmaking.GetLobbyOwner(lobbyId); CSteamID hostId = SteamMatchmaking.GetLobbyOwner(lobbyId);
KCClient.Connect(hostId.ToString()); KCClient.Connect(hostId.ToString());
//UIManager.Singleton.LobbyEntered();
} }
public void LeaveLobby() public void LeaveLobby()
{ {
Main.helper.Log("LeaveLobby called - cleaning up connection state"); //NetworkManager.Singleton.StopServer();
//NetworkManager.Singleton.DisconnectClient();
try
{
// Disconnect client first
if (KCClient.client != null && (KCClient.client.IsConnected || KCClient.client.IsConnecting))
{
Main.helper.Log("Disconnecting client...");
KCClient.client.Disconnect();
}
// Stop server if running
if (KCServer.IsRunning)
{
Main.helper.Log("Stopping server...");
KCServer.server.Stop();
}
// Leave Steam lobby
if (lobbyId.IsValid())
{
Main.helper.Log("Leaving Steam lobby...");
SteamMatchmaking.LeaveLobby(lobbyId); SteamMatchmaking.LeaveLobby(lobbyId);
}
// Clear player data if (KCClient.client.IsConnected)
Main.helper.Log("Clearing player data..."); KCClient.client.Disconnect();
Main.helper.Log("clear players");
Main.kCPlayers.Clear(); Main.kCPlayers.Clear();
Main.clientSteamIds.Clear();
// Clear UI
LobbyHandler.ClearPlayerList(); LobbyHandler.ClearPlayerList();
LobbyHandler.ClearChatEntries(); LobbyHandler.ClearChatEntries();
Main.helper.Log("end clear players");
// Reset flags if (KCServer.IsRunning)
KCServer.server.Stop();
Main.TransitionTo(MenuState.ServerBrowser);
ServerBrowser.registerServer = false; ServerBrowser.registerServer = false;
loadingSave = false;
Main.helper.Log("Lobby cleanup completed successfully");
// Transition back to server browser
Main.TransitionTo(MenuState.ServerBrowser);
}
catch (Exception ex)
{
Main.helper.Log("Error during LeaveLobby:");
Main.helper.Log(ex.Message);
Main.helper.Log(ex.StackTrace);
// Still try to transition back even if cleanup failed
Main.TransitionTo(MenuState.ServerBrowser);
}
} }
} }
} }

View File

@@ -299,13 +299,35 @@ namespace KCM
try 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++) for (int i = 0; i < kcmUICanvas.transform.childCount; i++)
Destroy(kcmUICanvas.transform.GetChild(i).gameObject); Destroy(kcmUICanvas.transform.GetChild(i).gameObject);
kcmUICanvas.name = "KCMUICanvas"; 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; KCMUICanvas = kcmUICanvas.transform;
@@ -322,6 +344,8 @@ namespace KCM
serverLobbyPlayerRef = serverLobbyRef.transform.Find("Container/PlayerList/Viewport/Content"); serverLobbyPlayerRef = serverLobbyRef.transform.Find("Container/PlayerList/Viewport/Content");
serverLobbyChatRef = serverLobbyRef.transform.Find("Container/PlayerChat/Viewport/Content"); serverLobbyChatRef = serverLobbyRef.transform.Find("Container/PlayerChat/Viewport/Content");
serverLobbyRef.SetActive(false); serverLobbyRef.SetActive(false);
serverBrowserRef.transform.SetAsLastSibling();
serverLobbyRef.transform.SetAsLastSibling();
//browser.transform.position = new Vector3(0, 0, 0); //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) private void Preload(KCModHelper helper)
{ {
helper.Log("Hello?"); helper.Log("Hello?");

View File

@@ -21,12 +21,12 @@ namespace KCM.ServerLobby
public void Start() public void Start()
{ {
banner = transform.Find("PlayerBanner").GetComponent<RawImage>();
SetValues(); SetValues();
InvokeRepeating("SetValues", 0, 0.25f); InvokeRepeating("SetValues", 0, 0.25f);
banner = transform.Find("PlayerBanner").GetComponent<RawImage>();
transform.Find("PlayerBanner").GetComponent<Button>().onClick.AddListener(() => transform.Find("PlayerBanner").GetComponent<Button>().onClick.AddListener(() =>
{ {
Main.TransitionTo(MenuState.NameAndBanner);//ChooseBannerUI Hooks required, as well as townnameui Main.TransitionTo(MenuState.NameAndBanner);//ChooseBannerUI Hooks required, as well as townnameui
@@ -37,8 +37,15 @@ namespace KCM.ServerLobby
{ {
try try
{ {
KCPlayer player; // First check if the client still exists
Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(Client).steamId, out player); if (!Main.TryGetPlayerByClientID(Client, out KCPlayer player) || player == null)
{
// Client no longer exists, stop the repeating invoke and destroy this entry
CancelInvoke("SetValues");
Destroy(gameObject);
return;
}
transform.Find("PlayerName").GetComponent<TextMeshProUGUI>().text = player.name; transform.Find("PlayerName").GetComponent<TextMeshProUGUI>().text = player.name;
transform.Find("Ready").gameObject.SetActive(player.ready); transform.Find("Ready").gameObject.SetActive(player.ready);

View File

@@ -60,8 +60,6 @@ namespace KCM
Falle Falle
} }
bool awake = false;
public void Start() public void Start()
{ {
Main.helper.Log("ServerLobby start called"); Main.helper.Log("ServerLobby start called");
@@ -144,7 +142,6 @@ namespace KCM
{ {
Main.helper.Log("Disable all"); Main.helper.Log("Disable all");
//StartGameButton.gameObject.SetActive(false);
StartGameButton.onClick.RemoveAllListeners(); StartGameButton.onClick.RemoveAllListeners();
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Ready"; StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Ready";
StartGameButton.onClick.AddListener(() => StartGameButton.onClick.AddListener(() =>
@@ -189,6 +186,32 @@ namespace KCM
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Start"; StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Start";
StartGameButton.onClick.AddListener(() => 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(); new StartGame().SendToAll();
if (PlacementType.value == 0 && !LobbyManager.loadingSave) if (PlacementType.value == 0 && !LobbyManager.loadingSave)

View File

@@ -1,4 +1,4 @@
using KCM.Packets; using KCM.Packets;
using KCM.Packets.State; using KCM.Packets.State;
using KCM.StateManagement.Observers; using KCM.StateManagement.Observers;
using System; using System;
@@ -6,12 +6,15 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using UnityEngine;
using static KCM.StateManagement.Observers.Observer; using static KCM.StateManagement.Observers.Observer;
namespace KCM.StateManagement.BuildingState namespace KCM.StateManagement.BuildingState
{ {
public class BuildingStateManager 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) public static void BuildingStateChanged(object sender, StateUpdateEventArgs args)
{ {
@@ -23,9 +26,29 @@ namespace KCM.StateManagement.BuildingState
try try
{ {
Observer observer = (Observer)sender; Observer observer = (Observer)sender;
Building building = (Building)observer.state; 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); //Main.helper.Log("Should send building network update for: " + building.UniqueName);
new BuildingStatePacket() new BuildingStatePacket()

View File

@@ -15,6 +15,11 @@ namespace KCM.UI
class KaC_Button class KaC_Button
{ {
public Button Button = null; 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 public string Name
{ {
@@ -84,14 +89,18 @@ namespace KCM.UI
set => Transform.SetSiblingIndex(value); set => Transform.SetSiblingIndex(value);
} }
public KaC_Button(Transform parent = null) public KaC_Button(Transform parent = null) : this(null, parent) { }
{
Button b = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").GetComponent<Button>();
if (parent == null) public KaC_Button(Button b, Transform parent = null)
Button = GameObject.Instantiate(b); {
else var templateButton = ResolveTemplateButton(b);
Button = GameObject.Instantiate(b, parent);
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>()) foreach (Localize Localize in Button.GetComponentsInChildren<Localize>())
GameObject.Destroy(Localize); GameObject.Destroy(Localize);
@@ -99,20 +108,27 @@ namespace KCM.UI
Button.onClick = new Button.ButtonClickedEvent(); Button.onClick = new Button.ButtonClickedEvent();
} }
public KaC_Button(Button b, Transform parent = null) private static Button ResolveTemplateButton(Button providedButton)
{ {
if (b == null) if (providedButton != null)
b = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").GetComponent<Button>(); return providedButton;
if (parent == null) foreach (var path in ButtonPaths)
Button = GameObject.Instantiate(b); {
else var transform = Constants.MainMenuUI_T?.Find(path);
Button = GameObject.Instantiate(b, parent); if (transform == null)
continue;
foreach (Localize Localize in Button.GetComponentsInChildren<Localize>()) var button = transform.GetComponent<Button>();
GameObject.Destroy(Localize); 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() public override string ToString()

BIN
linux/linux Normal file

Binary file not shown.

BIN
osx/osx Normal file

Binary file not shown.

Binary file not shown.

BIN
win32/win32 Normal file

Binary file not shown.

BIN
win64/win64 Normal file

Binary file not shown.