From 095caa35de647dcd6956d21bb7031f63de559ecd Mon Sep 17 00:00:00 2001 From: eaxiumnet Date: Mon, 17 Aug 2026 14:05:15 +0200 Subject: [PATCH] Fix district registration teardown and re-registration reply framing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses part 2 of #10 (WorldServer side). Part 1 (DistrictServer world-reconnect loop) is unchanged by this commit. Listener.cs — the disconnect teardown iterated DistrictsTcp in insertion order, logged from an inner loop, then removed `dtcp.Value` and broke out of the outer loop after the FIRST entry. It therefore only did the right thing when the disconnecting district happened to be the first one ever registered in the world process. Otherwise it removed the wrong district's code from Districts and left the actual dead district registered forever, so its replacement was rejected until the WorldServer itself restarted. Replaced with a direct DistrictsTcp.TryGetValue(tcpClient) lookup that removes exactly the dying connection's registration from both dictionaries. DistrictsTcp.Remove now happens inside the same guarded block, which also stops dead TcpClient objects from accumulating on re-registration. RegisterDistrict.cs — two defects on the collision path: * The same-IP replacement path sent 0x30 0x31 and then fell through to the normal 0x30 0x33 success reply, so one registration produced two 2-byte messages. The DistrictServer's reply parser reads a single record, so the trailing 0x33 leaked into its control-record stream and killed the connection loop. Now the stale entry is replaced and only the regular 0x33 success reply is sent. * The different-IP rejection path sent 0x30 0x32 and then `break`, which fell through to Districts.Add(code, district) — registering the very district it had just rejected and throwing on the duplicate key while leaving the socket open. Now it disconnects and returns. Verified: MSBuild Release, 0 warnings, 0 errors. Co-Authored-By: Claude Opus 5 --- WorldServer/District/Listener.cs | 25 ++++++++++++++---------- WorldServer/District/RegisterDistrict.cs | 17 ++++++++++++++-- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/WorldServer/District/Listener.cs b/WorldServer/District/Listener.cs index b740271..c0eed30 100644 --- a/WorldServer/District/Listener.cs +++ b/WorldServer/District/Listener.cs @@ -121,25 +121,30 @@ private void handleDistrict(object client) } try { + // Issue #10: the old teardown iterated DistrictsTcp in insertion + // order, broke after the FIRST entry, and removed the code of + // whichever district registered first — not the dying one. With + // multiple districts the dead entry stayed listed and its + // re-registering replacement was rejected until a world restart. lock (DistrictsTcp) { - foreach (KeyValuePair dtcp in DistrictsTcp) + uint code; + if (DistrictsTcp.TryGetValue(tcpClient, out code)) { lock (Districts) { - foreach (KeyValuePair dis in Districts) + District dis; + if (Districts.TryGetValue(code, out dis)) { - if (dis.Key == dtcp.Value) - { - Log.Error("Listener", dis.Value.ToString() + " disconnected!"); - break; - } + Log.Error("Listener", dis.ToString() + " disconnected!"); } - Districts.Remove(dtcp.Value); - break; + // Remove exactly this connection's registration. + // Idempotent: harmless when a re-registration already + // replaced the entry after the old socket died. + Districts.Remove(code); } + DistrictsTcp.Remove(tcpClient); } - DistrictsTcp.Remove(tcpClient); } tcpClient.Close(); } diff --git a/WorldServer/District/RegisterDistrict.cs b/WorldServer/District/RegisterDistrict.cs index 43a0110..38bc7f8 100644 --- a/WorldServer/District/RegisterDistrict.cs +++ b/WorldServer/District/RegisterDistrict.cs @@ -64,7 +64,15 @@ public static void Register(District district, TcpClient tcpClient, int type, by { if (dist.Value.IP == ip && Program.districtsListener.Districts.ContainsKey(code)) { - district.tcp.Client.Send(new byte[] { 0x30, 0x31 }); + // Same-IP re-registration: replace the stale + // entry and reply with a SINGLE success byte-pair + // (0x33), identical to a fresh registration. The + // old 0x31 prelude made the reply TWO 2-byte + // messages for one registration; the + // DistrictServer's one-reply parser could not + // frame it (the trailing 0x33 leaked into its + // control-record stream and killed the + // connection loop). Program.districtsListener.Districts.Remove(code); break; } @@ -72,7 +80,12 @@ public static void Register(District district, TcpClient tcpClient, int type, by { Log.Error("RegisterDistrict", "Fail try of district registration that already exists!"); district.tcp.Client.Send(new byte[] { 0x30, 0x32 }); - break; + // Reject cleanly: do NOT fall through to the + // register+0x33 below. The old fall-through added + // the rejected district anyway and crashed on the + // duplicate key, leaving the socket open. + district.tcp.Client.Disconnect(true); + return; } } Program.districtsListener.Districts.Add(code, district);