---
title: "Tracking a proprietary MMO's binary format across patches"
description: "The client of a live MMO I've been poking at stores its game data in .bin files using a positional format: no tags, no field names, just a sequence…"
date: "2026-06-21T09:00:00.000Z"
updated: "2026-08-24T15:05:22.350Z"
locale: "en"
canonical: "https://ninhache.fr/en/blog/suivre-format-binaire-mmo"
author: "Néo Almeida"
tags: "reverse-engineering, binaire, tooling"
categories: "dev"
---

# Tracking a proprietary MMO's binary format across patches

The client of a live MMO I've been poking at stores its game data in positional-format `.bin` files: no tags, no field names, just a sequence of bytes read in a fixed order by a hand-written reader. The problem is that this client is obfuscated through automatic symbol renaming: with every re-build, class and method names change, a class that was called `p` last week is called `epH` this week, and nothing guarantees it still plays the same role.

The game updates regularly, and with every patch, a handful of binary structures drift: a field added, another removed, the order changed. If the external reader doesn't keep up, it keeps decoding, but silently shifts everything from the first mis-interpreted byte onward.

I built a tool to stop redoing this work by hand at every patch: detect the drift, find the right reader in the decompiled client without ever relying on an obfuscated name (since it won't survive the next patch), and preserve the field names a human chose across versions.

## The volatile-name trap

The first temptation, faced with a decompiled dump, is to look up a class by its name. That works once, then the next patch renames everything and you have to start over. The only thing stable between two versions is the structure: how many fields, in what order, of what type. So I took the opposite approach: never hard-code an obfuscated name anywhere, and dynamically recover the three pieces I need every time.

**Registry enum**

First, the registry enum, the one that maps a numeric file identifier to a type constant. I don't know its name, but I know it's instantiated once per declared constant, with a call like `= new X(id)`. Whichever class gets constructed this way the most times across the whole dump is the one. Once found, a regex over its own source code yields the id-to-constant table.

**Buffer legend**

Next, the buffer's "legend", i.e. the mapping between a read method (`bHA()`, for instance) and the type it returns. I read it through textual reflection on the buffer class itself: any public no-argument method that returns a known primitive type goes into the legend. No need to know that `bHA` means "read an integer": the obfuscated source code tells me directly.

**Candidate readers**

Finally, the reader(s) for a given id: classes that reference the enum constant and that have a `void a(Buffer p)` method. There are usually two, a server variant and a client variant, almost identical but not quite. That's where it gets interesting.

## Transcribing a method without writing a Java parser

Once you have the body of the `a()` method in hand, it needs to be transcribed into an ordered sequence of schema operations: read an integer, read a string, loop N times over a sub-structure, read an optional field based on a flag. I deliberately chose not to write a real Java parser. The code is already compiled and then decompiled, so it's syntactically homogeneous and predictable in shape (no exotic syntactic sugar). Brace matching to delimit blocks, combined with targeted regexes, is enough to spot the three patterns that matter:

**Loop**

A `for` loop preceded by an integer that serves as a counter (I collapse the pair into a single "loop" node that folds the counter into the structure).

**Optional flag**

An `if` whose condition, or the local variable right before it, is a boolean or a byte (I collapse it into a single-byte flag).

**Sub-structure**

A `.a(param)` call on a freshly constructed object, which signals a sub-structure to inline recursively.

**Trap**

This is admittedly fragile; a real AST parser would be more robust against exotic code. But the generated code is stable in shape from one version to the next (only the names move), so the trade-off holds: the tool correctly transcribes the vast majority of readers, and for the rare failures, it says so clearly instead of silently producing a wrong schema.

```json title="schema node"
{"path": "12.3", "kind": "read", "type": "int", "obf": "epH", "name": "familyId"}
```

That's a node from the knowledge layer I get to below: a path in the schema tree, a type, the current obfuscated name (just for visual reference), and the semantic name I assigned.

## The referee: byte-exact verification

What's left is the problem of choosing between the two candidates, server and client. Nothing in the code says which of the two matches the format actually shipped in the installed files. The solution I settled on is brutal and foolproof: try each candidate against the real installed binary data, and keep whichever one decodes every entry down to the last byte. Concretely, each entry declares its own size; if the number of bytes consumed matches the declared size exactly for every entry, the candidate is the right one. Otherwise, it fails immediately, often on the very first entry.

In a real case I hit after a patch, one candidate decoded 175901 entries out of 175901, the other zero. No ambiguity possible: the exact size is an oracle with no appeal. This test is what turns a structural guess into verifiable certainty, without ever needing to understand what the code actually does.

_Entries decoded down to the last byte, out of 175901 entries tested._

`make bump` chains all of this together after a patch:

**Identify**

Identify the ids whose reader has stopped matching.

**Retranscribe**

Retranscribe both candidates from the fresh dump.

**Test**

Test them against the installed bins.

**Pin**

Pin the winner in an override file.

Unresolved ids stay listed, not swept under the rug.

## Anchoring human knowledge by position, not by name

The last piece is the knowledge layer: the semantic names I assign to each field ("familyId", not "epH"). The obfuscated name doesn't survive anything, so I only use it as a display reference. What truly anchors a name is its structural position in the schema tree (the path, something like "12.3") combined with its type. When a new version comes out and the structure is retranscribed from scratch, with completely different obfuscated names, I re-map the old names onto the new fields by comparing (position, type). If a field hasn't moved and keeps the same type, it automatically gets its human name back. If it changed position or disappeared, the tool flags it as "needs review" instead of guessing.

Each game version becomes a frozen snapshot of this knowledge: I can compare two versions and see precisely which fields were added, which ones disappeared, and for the latter, what name they had before they left. It's a schema history, not just a current state.

## What I take away from this

**Defined types**

161 possible types in the registry enum, of which 129 are actually shipped in the installed files, and all of them decode without error.

**Transcribed schemas**

About 341 KB of JSON, generated from a combination of 112 hand-ported reference readers and automatic transcriptions for the more recent structures.

**Dependencies**

The interface used to track each file's state and name the fields runs locally, with no external dependency, just the standard library.

**Lesson**

The core lesson is that anchoring knowledge to a name, in a system where the name is disposable, amounts to building on sand. Anchoring to a property that doesn't move (here, the position in a positional tree, combined with the type), is what lets a human naming effort survive dozens of patches without having to redo everything each time.
