What is this?
A community effort to figure out how New World's online protocol actually works on the wire — with the goal of eventually building a private server that the game client can connect to.
Right now we're in the static-analysis phase: pulling apart a captured login session and the game binary, message by message. Every wire-format type the client sent or received in the captured session has been catalogued, and our codec library can read and re-emit them byte-for-byte identically.
The project is independent and not affiliated with Amazon Games. All the work below is hand-written analysis — everything's open-source on GitHub.
When you log into New World, the client and server walk through a sequence of internal states before you can actually play. Here's the rough breakdown of each step and where we're stuck.
Charts are computed from the captured DTLS replay (~2 minutes of real login + a snapshot of post-login traffic).
Top message types by volume
Direction split
Codec coverage depth
Top types vs. coverage
Test-suite growth over wakes
site/data.json records the test count at that point. Wake numbers come from commit messages. The jump at wake 164 (347→410) reflects a scope change: the counter was originally only the codec suite, now covers all of server/javelin/ including the wake-157 shadow-decode + wake-162 tooling tests.Live-decoder coverage over wakes
Each dot is a message on the wire. X-axis is the order it was sent; Y-axis is the message type. Blue dots are server→client (R), green dots are client→server (W). The "shape" of a typical login is immediately visible — the early V3 handshake, the bulk world data around seq 6, then the steady heartbeat patter.
Captured session timeline (177 messages)
Each captured message that carries an identity bundle includes
an 8-byte sub_system_id tag. When two or more
wire-types share a tag, they're part of the same in-session
sub-system — different message kinds in one conceptual flow.
These groupings come from
analysis/identity_bundle_correlation.md
(wake 121).
Source (this branch): github.com/nw-private-server/first-light @ claude/vacation-2026-05-06
What's a wire-type?
0x15d is a heartbeat
ping, 0x65c is bulk world data, 0x18a6
is a session init beacon. The table below catalogs all 40 wire
types we observed in a captured login session, plus how each
one is structured (codec module), how often it appeared, and
whether we've confirmed its name in the binary. Click the
📋 on any row to copy a one-line
command for examining a real captured instance with
tools/decode_message.py.
| Type | N | Dir | Bytes | Name | UUID | Codec |
|---|
What's a decompile?
FUN_146...) are
runtime virtual addresses in the binary.
Every analysis/*.md document committed to the repo. Click
a title to read the full writeup on GitHub.
How does the codec library actually work?
The game and server talk to each other in a stream of binary messages. Each one is just bytes on the wire — no labels, no structure visible to a network sniffer. Our codec library's job is to reverse that: take a body of raw bytes and turn it into a typed Python object we can reason about.
Here's a worked example using a real captured message — a
heartbeat ping (wire type 0x15d) that the server
sends every few seconds while the player is connected.
The 12 bytes the network sees. Meaningless on their own.
00 01 9d 05 00 03 6e f6 af 91 2d 74
The first 4 bytes are a small header that names the message
type. The protocol encodes the type-id by splitting it into
two pieces: a "marker" pair 00 01 followed by
(type_id & 0x3f) | 0x80 and
type_id >> 6.
For 9d 05: (0x9d & 0x7f) | (0x05 << 6) = 0x15d.
00 01 9d 05 00 03 6e f6 af 91 2d 74
└── type 0x15d ──┘
server.javelin.dispatch.decode_replay_message(0x15d, "R", body)
looks up 0x15d in its table and routes to
heartbeat_15d.decode_ping. The dispatcher knows
all 40 captured wire-types in the replay (plus
0x5d1 for the state-10 unblock).
The codec knows the heartbeat ping's wire layout: after the 4-byte header, two big-endian 32-bit integers — a counter and a nonce.
00 01 9d 05 00 03 6e f6 af 91 2d 74 ↑──────────↑ ↑──────────↑ counter nonce = 225014 = 2945527156
The codec returns a Python dataclass that's easy to inspect and re-emit. Round-trip is byte-identical: encoding this back produces the original 12 bytes.
HeartbeatPing15D(
counter = 225014,
nonce = 2945527156,
)
The same dataclass renders as JSON when needed (e.g. for the
tools/decode_message.py --json CLI or the
dashboard's message inspector).
{
"counter": 225014,
"nonce": 2945527156
}
Heartbeats are about as simple as it gets. Most captured types
have more pieces. Here's the same 6-step walkthrough for an
InitMessage18A6 (wire type 0x18a6) — a
40-byte server-side beacon with an identity bundle and a counter
that the client echoes back as 0x1a59.
00 01 a6 62 f8 cb ed 57 c6 8b 18 f4 bf 85 31 4b bc 4a 95 1a 01 01 00 00 9c fa 58 61 78 14 69 f2 65 03 00 00 00 00 02 01
Same decode rule as before:
(0xa6 & 0x7f) | (0x62 << 6) = 0x18a6.
00 01 a6 62 …
└── type 0x18a6 ─┘
Most non-trivial messages carry a 16-byte identity tag right after the type header. It splits as two 8-byte halves:
f8 cb ed 57 c6 8b 18 f4 bf 85 31 4b bc 4a 95 1a └─ first_uuid_half ─┘ └ session_uuid_lower ┘
The session_uuid_lower (yellow) is constant
across this whole session — see the
Wire-type families panel for the
7 sub-system families it appears in.
The remaining 20 bytes are 5 typed fields the codec knows the layout of:
… 01 01 00 00 9c fa 58 61 78 14 69 f2 65 03 00 00 00 00 02 01 ↑ ↑ ↑ ↑ ↑ flags pad second_id (8 bytes) build_ver counter+pad = 257 = 9cfa58617814 69f2 = 869 = 1
InitMessage18A6(
first_uuid_half = b'\xf8\xcb\xedW\xc6\x8b\x18\xf4',
session_uuid_lower = b'\xbf\x851K\xbcJ\x95\x1a',
second_id = b'\x9c\xfaXax\x14i\xf2',
counter = 1,
flags = 257,
build_version = 869,
)
The counter field is what makes
InitMessage18A6 a counter-coupled pair
with 0x1a59 (SessionSubkeyBeacon). Each
0x18a6 the server sends carries an incrementing
counter; the client's 0x1a59 reply echoes the
same counter so the server can confirm the link is alive.
Both sit in the same f8cbed57c68b18f4
sub-system family — see the Wire-type families panel for
the cross-correlation table from wake 121.
Most messages aren't this simple. A typical body has:
- Type header (4 bytes) — same shape as above.
- Identity bundle (16 bytes) — an 8-byte
sub_system_idtag plus an 8-bytesession_uuid_lower. Messages that share asub_system_idbelong to the same sub-system family — see Wire-type families on the Overview tab. - Structured payload — floats, durations, hashes, counters, etc.
- Trailer — often a CRC or fixed shared bytes.
Every captured message in the replay round-trips through this pipeline byte-for-byte — see the audit-gap counter on the Overview row (0/0 means every codec has both decode-rejection and populated-encode tests).
Three short ramps for new contributors. Each one points at the existing artifact you'd extend or copy.
Pick an uncovered wire-type (see
codec_coverage.md;
currently 40/40 captured, but new captures may surface new types).
Copy the layout of session_clock_beacon.py for a
fixed-shape body or asset_blob_16a0.py for a
variable-length one. Register it in
dispatch.py
DECODERS/ENCODERS. Add a structural-rejection test + a
populated round-trip test to
test_codecs.py.
The dispatcher full-replay test will catch a missed type-id
automatically.
For codec-level coverage, follow the round-trip + structural-
rejection pattern in test_codecs.py. For wider
coverage (dispatcher, CLI, tooling), see
test_shadow_decode.py
(mock-self pattern with a recording log handler) or
test_build_tools.py
(pure-helper unit tests). Run
.venv/bin/pytest server/javelin/ -q to verify;
the badge on the README auto-updates on push.
Run .venv/bin/python3 tools/build_site.py to
rebuild site/data.json + the shields.io badge
endpoint JSONs. Pages auto-deploys on every push to the
working branch, so a commit-and-push refreshes the live
dashboard within ~2 min. The public API reference is
regenerated by
build_api_reference.py —
re-run after touching __init__.py or a class
docstring.
Bigger-picture guidance lives in CONTRIBUTING.md and the per-module server/javelin/README.md overview.
Live decoder
server/javelin/. Currently supports four simple
shapes; click a preset below to populate with a real captured
payload. (Press Ctrl/Cmd+Enter in the
hex box to decode.)
(pick a preset or paste hex and press Decode)
Show wire-types not yet in the live decoder
Byte-pattern search
bf85314b (session_uuid_lower prefix for this session) appears in many bodies — try it.