NW Private-Server RE Dashboard

Loading…
Reverse-engineering · independent project

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.

Recent activity
    test cross-checks, lockdowns, invariants · site Findings cards, decoder coverage, dashboard · code rep_responder, codec, phase-2 work · docs retrospective, README, worklog updates
    Full worklog →
    By the numbers
    Where we are in the connection

    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.

    Understood + working Current blocker Beyond the blocker
    A look at the captured traffic

    Charts are computed from the captured DTLS replay (~2 minutes of real login + a snapshot of post-login traffic).

    Top message types by volume

    Most common messages in the replay. Type IDs are the wire identifiers; names appear once we've confirmed them.

    Direction split

    R = server→client, W = client→server. A typical session is mostly R as the server pushes world state.

    Codec coverage depth

    All 40 captured types have a codec; this shows how much structure each codec captures.

    Top types vs. coverage

    High-volume types are usually the most worth fully decoding. Color = codec depth.

    Test-suite growth over wakes

    Each commit that touched 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

    Captured wire-types decodable directly from the Explore tab. Wake 152 introduced the live decoder at 6/40; wake 217 reached 36/40 (90.0%) — the design floor per the wake-221 decision doc.
    The captured login session, message-by-message

    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)

    Hover/tap a dot for a quick summary. Click a dot to inspect that message's decoded fields below — every dot is a real captured message routed through the codec library's central dispatcher.
    Click any dot in the timeline above to see that message's decoded fields and raw bytes.
    Wire-type families

    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).

    How we got here
    FAQ

    Source (this branch): github.com/nw-private-server/first-light @ claude/vacation-2026-05-06

    What's a wire-type?
    Every message New World sends or receives over the network has a numeric "type ID" stamped at the start. Different IDs mean different message kinds — 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.
    TypeNDirBytes NameUUIDCodec
    What's a decompile?
    The game ships as a compiled binary; we use Ghidra to translate individual functions back into readable C-like code so we can understand what they do. The 39 files below are functions we've characterized so far, grouped by purpose — the state-machine ones are the most actively-used (they reveal the connection blocker the project is trying to crack). Each file is a snippet of decompiled C; the addresses (FUN_146...) are runtime virtual addresses in the binary.
    All decompiles
    Curated highlights
    All analysis writeups

    Every analysis/*.md document committed to the repo. Click a title to read the full writeup on GitHub.

    For curious readers

    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.

    1
    Raw bytes off the wire

    The 12 bytes the network sees. Meaningless on their own.

    00 01 9d 05  00 03 6e f6  af 91 2d 74
    2
    Parse the typed envelope header

    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 ──┘
    3
    Dispatch to the right codec

    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).

    4
    Read each field from the body

    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
    5
    Return a typed dataclass

    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,
    )
    6
    JSON output for piping

    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
    }
    A more involved example: identity-bundle messages

    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.

    1
    Raw bytes off the wire (40 bytes)
    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
    2
    Type header → 0x18a6

    Same decode rule as before: (0xa6 & 0x7f) | (0x62 << 6) = 0x18a6.

    00 01 a6 62  …
    └── type 0x18a6 ─┘
    3
    Identity bundle (next 16 bytes)

    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.

    4
    Structured tail

    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
    5
    Dataclass output
    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,
    )
    6
    Why this one matters

    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.

    More involved messages have more pieces

    Most messages aren't this simple. A typical body has:

    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).

    Want to contribute? Pick a path.

    Three short ramps for new contributors. Each one points at the existing artifact you'd extend or copy.

    Add a new codec

    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.

    Add a test

    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.

    Refresh the dashboard

    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
    Paste a captured message body as hex, pick its wire-type, and see the decoded fields. The parsing runs entirely in your browser — same wire-layout rules as the Python codecs in 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.)
    Try a captured payload:
    (pick a preset or paste hex and press Decode)
    Byte-pattern search
    Search all 177 captured message bodies for a hex byte pattern. Useful for spotting recurring protocol structures across message types — e.g. session-uuid fragments, type-header bytes, or magic numbers. Spaces in the query are ignored; matching is case-insensitive.

    bf85314b (session_uuid_lower prefix for this session) appears in many bodies — try it.
    Try: