When building multiplayer for the Obsidian Tower, one of the fundamental challenges is synchronizing the game world between players. The host generates each floor procedurally, and then needs to send the complete floor definition to every party member so everyone sees the same room layout. This turned out to be much harder than expected.
The Initial Symptom
When testing with multiple players, party members would receive corrupted floor data. The rooms they saw didn't match what the host generated — walls were missing, doors were in the wrong places, and sometimes the floor was just completely broken. At first glance, it appeared to be a message truncation problem.
Looking at the data being sent and received, it seemed like if the floor definition was 300 characters long, but the max is 200, the last 100 characters would be dropped. The floor data was being cut off mid-transmission.
First Attempts at Fixing
My initial approach was to reduce the size of the data being sent. The original floor definitions used full tile names like "WALL, GROUND" for each cell in the grid. I compressed these down to single characters — "w" for wall, "g" for ground, and so on — turning "wgggw" into a fraction of the original message size. Something like "WALL, GROUND, GROUND, GROUND, WALL" became simply "wgggw".
This helped reduce the overall payload, but it didn't fully solve the problem. For larger floors, the data was still getting corrupted.
Next, I tried splitting the floor data across multiple WebSocket calls. Instead of sending one large message, I broke the floor definition into 9 separate messages — one for each section of the grid — and had the client reassemble them. This was more complex but seemed like it should circumvent any message size limits.
The Real Root Cause
After more debugging, I finally got a clear error message that pointed to the actual problem:
Expected BEGIN_OBJECT but was STRING at line 1 column 42 path $.definition.doors
This was a deserialization error. The issue had nothing to do with message size or truncation. The problem was in how the floor data was being serialized and deserialized — specifically, how a custom Pair object was being used as a map key.
The floor definition included a map that used Pair objects (representing x,y coordinates) as keys. When this map was serialized to JSON, the Pair objects were converted to their default string representation rather than being properly serialized as structured objects. On the receiving end, the JSON deserializer expected a proper object but found a string, and the whole thing fell apart.
The Resolution
The fix was to implement middleware that properly converts Pair objects to and from strings during serialization and deserialization. I wrote custom serializers and deserializers that could handle the Pair-to-string conversion in a way that was reversible and consistent.
With the serialization issue resolved, the floor data transmitted correctly in a single WebSocket call. I was able to remove the 9-message splitting workaround and the single-character compression — though I kept the compression since it still reduced bandwidth, just no longer out of necessity.
As for the apparent truncation I saw earlier? That turned out to be just a logging display issue. The debug logs were truncating long messages for readability, which made it look like the data itself was being cut off. The data was actually arriving in full — it just couldn't be deserialized properly.
This was a good lesson in not trusting initial assumptions. What looked like a network transport problem was actually a data format problem, and what looked like truncation was actually a logging artifact.