diff --git a/KCClient.cs b/KCClient.cs index fa52f44..8ba4831 100644 --- a/KCClient.cs +++ b/KCClient.cs @@ -19,7 +19,7 @@ namespace KCM { public class KCClient : MonoBehaviour { - public static Client client = new Client(Main.steamClient); + public static Client client; public string Name { get; set; } @@ -28,6 +28,33 @@ namespace KCM 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.ConnectionFailed += Client_ConnectionFailed; client.Disconnected += Client_Disconnected; @@ -36,33 +63,44 @@ namespace KCM private static void Client_Disconnected(object sender, DisconnectedEventArgs e) { - Main.helper.Log("Client disconnected event start"); + Main.helper.Log($"Client disconnected event start - Reason: {e.Reason}"); 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) { - Main.helper.Log(e.Message.ToString()); + Main.helper.Log("Processing disconnect message..."); MessageReceivedEventArgs eargs = new MessageReceivedEventArgs(null, (ushort)Enums.Packets.ShowModal, e.Message); if (eargs.MessageId == (ushort)Enums.Packets.ShowModal) { ShowModal modalPacket = (ShowModal)PacketHandler.DeserialisePacket(eargs); - modalPacket.HandlePacketClient(); } } - else + 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 + { + Main.helper.Log("Voluntary disconnect - no modal shown"); + } } catch (Exception ex) { - Main.helper.Log("Error handling disconnection message"); - Main.helper.Log(ex.ToString()); + Main.helper.Log("Error handling disconnection message:"); + Main.helper.Log(ex.Message); + Main.helper.Log(ex.StackTrace); } Main.helper.Log("Client disconnected event end"); } @@ -88,7 +126,11 @@ namespace KCM public static void Connect(string 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() diff --git a/KCServer.cs b/KCServer.cs index 0648689..010ee77 100644 --- a/KCServer.cs +++ b/KCServer.cs @@ -18,83 +18,104 @@ namespace KCM { public class KCServer : MonoBehaviour { - public static Server server = new Server(Main.steamServer); + public static Server server; public static bool started = false; static KCServer() { - //server.registerMessageHandler(typeof(KCServer).GetMethod("ClientJoined")); + // Initialize server in static constructor + 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.ClientConnected += OnClientConnected; + server.ClientDisconnected += OnClientDisconnected; + } + + private static void OnClientConnected(object obj, ServerConnectedEventArgs ev) + { + Main.helper.Log("Client connected"); + + if (server.ClientCount > LobbyHandler.ServerSettings.MaxPlayers) + { + ShowModal showModal = new ShowModal() { title = "Failed to connect", message = "Server is full." }; + + showModal.Send(ev.Client.Id); + + server.DisconnectClient(ev.Client.Id); + return; + } + + ev.Client.CanQualityDisconnect = false; + + Main.helper.Log("Client ID is: " + ev.Client.Id); + + new ServerHandshake() { clientId = ev.Client.Id, loadingSave = LobbyManager.loadingSave }.Send(ev.Client.Id); + } + + private static void OnClientDisconnected(object obj, ServerDisconnectedEventArgs ev) + { + try + { + if (Main.clientSteamIds.ContainsKey(ev.Client.Id)) + { + new ChatSystemMessage() + { + Message = $"{Main.GetPlayerByClientID(ev.Client.Id).name} has left the server.", + }.SendToAll(); + + Main.kCPlayers.Remove(Main.GetPlayerByClientID(ev.Client.Id).steamId); + + var playerEntry = LobbyHandler.playerEntries + .Select(x => x.GetComponent()) + .Where(x => x.Client == ev.Client.Id) + .FirstOrDefault(); + + if (playerEntry != null) + Destroy(playerEntry.gameObject); + } + + Main.helper.Log($"Client disconnected. {ev.Reason}"); + } + catch (Exception ex) + { + Main.helper.Log($"Error handling client disconnect: {ex.Message}"); + } } public static void StartServer() { - server = new Server(Main.steamServer); - server.MessageReceived += PacketHandler.HandlePacketServer; + // Reinitialize server to ensure clean state + InitializeServer(); server.Start(0, 25, useMessageHandlers: false); - server.ClientConnected += (obj, ev) => - { - Main.helper.Log("Client connected"); - - if (server.ClientCount > LobbyHandler.ServerSettings.MaxPlayers) - { - ShowModal showModal = new ShowModal() { title = "Failed to connect", message = "Server is full." }; - - showModal.Send(ev.Client.Id); - - server.DisconnectClient(ev.Client.Id); //, PacketHandler.SerialisePacket(showModal) - return; - } - - ev.Client.CanQualityDisconnect = false; - - Main.helper.Log("Client ID is: " + ev.Client.Id); - - new ServerHandshake() { clientId = ev.Client.Id, loadingSave = LobbyManager.loadingSave }.Send(ev.Client.Id); - }; - - server.ClientDisconnected += (obj, ev) => - { - new ChatSystemMessage() - { - Message = $"{Main.GetPlayerByClientID(ev.Client.Id).name} has left the server.", - }.SendToAll(); - - Main.kCPlayers.Remove(Main.GetPlayerByClientID(ev.Client.Id).steamId); - Destroy(LobbyHandler.playerEntries.Select(x => x.GetComponent()).Where(x => x.Client == ev.Client.Id).FirstOrDefault().gameObject); - - Main.helper.Log($"Client disconnected. {ev.Reason}"); - }; - 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; } } private void Update() diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a0fa09 --- /dev/null +++ b/README.md @@ -0,0 +1,238 @@ +# Kingdoms and Castles Multiplayer Mod + +Ez a mod multiplayer funkcionalitást ad a Kingdoms and Castles játékhoz. + +## Legutóbbi javítások (2025-12-14) + +### Kapcsolati problémák javítása + +#### Probléma +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. + +#### Gyökérok +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. + +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. + +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. + +#### Megoldások + +##### 1. KCClient.cs - Javított újracsatlakozási képesség +**Változtatások:** +- Ú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: + - 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: + - Tisztítja a client state-et (clientSteamIds) + - Megkülönbözteti az önkéntes és nem várt disconnect-eket + - 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. diff --git a/RiptideSteamTransport/LobbyManager.cs b/RiptideSteamTransport/LobbyManager.cs index f159086..e6070cd 100644 --- a/RiptideSteamTransport/LobbyManager.cs +++ b/RiptideSteamTransport/LobbyManager.cs @@ -66,16 +66,11 @@ namespace Riptide.Demos.Steam.PlayerHosted if (callback.m_eResult != EResult.k_EResultOK) { - //UIManager.Singleton.LobbyCreationFailed(); Main.helper.Log("Create lobby failed"); return; } lobbyId = new CSteamID(callback.m_ulSteamIDLobby); - //UIManager.Singleton.LobbyCreationSucceeded(callback.m_ulSteamIDLobby); - - //NetworkManager.Singleton.Server.Start(0, 5, NetworkManager.PlayerHostedDemoMessageHandlerGroupId); - KCServer.StartServer(); @@ -92,16 +87,6 @@ namespace Riptide.Demos.Steam.PlayerHosted 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; } catch (System.Exception ex) @@ -122,8 +107,6 @@ namespace Riptide.Demos.Steam.PlayerHosted Main.helper.Log(ex.InnerException.StackTrace); } } - - //NetworkManager.Singleton.Client.Connect("127.0.0.1", messageHandlerGroupId: NetworkManager.PlayerHostedDemoMessageHandlerGroupId); } internal void JoinLobby(ulong lobbyId) @@ -145,31 +128,62 @@ namespace Riptide.Demos.Steam.PlayerHosted CSteamID hostId = SteamMatchmaking.GetLobbyOwner(lobbyId); KCClient.Connect(hostId.ToString()); - //UIManager.Singleton.LobbyEntered(); } public void LeaveLobby() { - //NetworkManager.Singleton.StopServer(); - //NetworkManager.Singleton.DisconnectClient(); - SteamMatchmaking.LeaveLobby(lobbyId); + Main.helper.Log("LeaveLobby called - cleaning up connection state"); - if (KCClient.client.IsConnected) - KCClient.client.Disconnect(); + try + { + // Disconnect client first + if (KCClient.client != null && (KCClient.client.IsConnected || KCClient.client.IsConnecting)) + { + Main.helper.Log("Disconnecting client..."); + KCClient.client.Disconnect(); + } - Main.helper.Log("clear players"); - Main.kCPlayers.Clear(); - LobbyHandler.ClearPlayerList(); - LobbyHandler.ClearChatEntries(); - Main.helper.Log("end clear players"); + // Stop server if running + if (KCServer.IsRunning) + { + Main.helper.Log("Stopping server..."); + KCServer.server.Stop(); + } - if (KCServer.IsRunning) - 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(); - Main.TransitionTo(MenuState.ServerBrowser); - ServerBrowser.registerServer = false; + // Reset flags + 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); + } } } }