Skip to content

About

Clientless Diablo II 1.14d Battle.net client (BNCS/MCP/BNFTP/D2GS) in Zig

Resources

Stars

12 stars

Watchers

1 watching

Forks

Latest commit

 

History

41 Commits

Folders and files

Repository files navigation

clientless

Discord

A clientless Diablo II 1.14d Battle.net client, written in Zig. It speaks the wire protocols directly — BNCS (login), MCP (realm/characters), BNFTP (file transfer), and the D2GS game protocol — with no game binary, no wine, and no graphics. One small static binary connects, authenticates, picks a realm, creates/selects a character, joins a game, and receives the live in-game packet stream.

If this is useful to you, you can sponsor the work.

Why this exists

This is the test client for jaenster/d2-dedicated-server — a clean-room Zig reimplementation of the Diablo II closed-realm servers (the realmd triplet: bnetd + d2cs + d2dbs, plus a headless game server).

To know that server actually works, you need something that talks the client side of the protocol. The real game client is a closed-source Windows binary that needs graphics, wine, and the proprietary data files — useless for automated testing. clientless is the answer: it drives the entire client→server handshake from a script, so the server's auth, realm, character, and game-join paths can be exercised in CI and by hand, on every commit, with no game install.

It's also a faithful, readable reference for how the 1.14d closed-Battle.net protocol works, and it runs against real Battle.net too (it was validated end-to-end against a live gateway).

What it can do

  • Version check — computes the modern base64 CheckRevision response (checkrev_core, the 1.14d CheckRevision.mpq algorithm) and the CD-key auth blocks for SID_AUTH_CHECK.
  • Accounts — log in to, or create, an account over OLS (Blizzard's broken-SHA-1 password hash).
  • Realm — list realms and log on to one, then over MCP: list/create/select characters, read the ladder.
  • Chat — enter chat, join a channel, send messages, /kick, and listen for events.
  • Game — create or join a game and enter it on the game server: GAMELOGON (0x68) → wait for the GS 0x02 LoadSuccess → JOINGAME (0x6b), then read the S→C game stream (opcode-framed; 0xAE huffman blobs are decompressed) — character assignment, act load, unit spawns, stat updates, quests.
  • BNFTP — discover and download server files (the bnftp subcommand).
  • Works against your own realm (realmd/pvpgn) and real Battle.net; handles both the modern base64 challenge and the legacy A=1 B=1 … formula; runs keyless on a permissive realm.

What it can't (and won't) do

  • It is not a bot. It performs the join handshake, parses the incoming packet stream, and prints what it sees — but it does not model game state (no player/unit/position tracking) and does not act in the world (no movement, no skills, no item pickup). It's a protocol harness, not an automation framework.
  • It doesn't stay in-game. After joining it reads the stream for a short window and exits; there's no long-lived session / keep-alive loop.
  • No native Windows binary. The socket layer is POSIX; on Windows run it via the container (Docker Desktop / WSL2). A Winsock port is possible but not done.
  • Classic challenge is a placeholder. Against the legacy A=1 B=1 … formula it sends a placeholder hash, which only a permissive realm accepts — it does not compute a real classic CheckRevision (that needs the game files).
  • No anti-cheat work. It does not run, forge, or bypass the lockdown / extra-work (Warden-style) modules. Game entry doesn't require them, so it simply doesn't engage them; it won't be extended to defeat them on a live service.
  • One session per run, single-threaded. No connection pooling, no concurrency.

Install

One line (Linux / macOS / FreeBSD) — detects your OS/arch, grabs the latest release binary:

curl -fsSL https://raw.githubusercontent.com/jaenster/d2-clientless/main/install.sh | sh

Or grab a tarball yourself from the Releases page — Linux (x86_64, aarch64, armv7, riscv64, static musl), macOS (x86_64, aarch64), and FreeBSD (x86_64, aarch64). Or run the container.

Build

Requires Zig 0.16. Produces a single binary:

zig build          # -> zig-out/bin/clientless
zig build test     # crypto unit tests (CheckRevision / CD-key / xSHA-1)

Usage

clientless <host> [product] [version] [options]      # BNCS / MCP / chat / game
clientless bnftp [options] <host> [product] [file]   # BNFTP file client

Run clientless with no arguments for the full, sectioned help. Common flags:

flag meaning
--keys K1[,K2] 26-char CD-key(s); omit on a permissive realm
--login acct:pass / --create acct:pass log in / register then log in
--game <name> (--gs-port <n>) create + join a game and enter it on the GS
--channel/--say/--kick/--listen <sec> chat
--verbyte <n> GAMELOGON version byte (default 14 = 1.14d)
--force-checkrev answer even if the version-check MPQ isn't CheckRevision.mpq
--sig0 / --delay <ms> / --verbose sigOk=0 / pace each step / hexdump all traffic

Examples

# version check only — no keys, no account
clientless useast.battle.net

# full session on your own realm: create a char, chat, read the ladder
clientless realm.example.com D2XP 1.14.3.71 --login me:pw --listen 20

# create the account, then create + enter a game
clientless realm.example.com D2XP 1.14.3.71 --create me:pw --game MyGame

# two clients chatting; the operator kicks the other
clientless realm.example.com --login op:pw --channel ops --kick rude

# live Battle.net with real CD-keys (your account, your responsibility)
clientless useast.battle.net D2XP 1.14.3.71 --keys K1,K2 --login acct:pw --game MyGame

# BNFTP: a file's size, then download it
clientless bnftp --head useast.battle.net D2XP CheckRevision.mpq
clientless bnftp --out-dir . useast.battle.net D2XP CheckRevision.mpq

How the game join works

GAMELOGON (0x68) carries the JOINGAME token + game hash + character name (the exact D2GSPacketClt0x68 field layout). The client first waits for the GS's 0xAF connection packet (like the real client's connecting loop), then sends 0x68, reads the S→C stream framed by opcode + the engine's size table (not length-prefixed), and sends JOINGAME (0x6b) only in response to the server's 0x02 LoadSuccess. Compressed 0xAE blobs are huffman-decompressed (src/huffman.zig) and their inner packets parsed.

The three hashes

Easy to conflate; they are distinct:

  1. CheckRevision — standard SHA-1. checkrev_core implements the 1.14d CheckRevision.mpq algorithm specifically; against any other version-check MPQ it refuses to answer (so it never sends a wrong response) unless --force-checkrev.
  2. CD-key block — standard SHA-1 over clientToken ++ serverToken ++ product ++ public ++ value.
  3. Account password — Blizzard's broken SHA-1 (src/xsha1.zig): single-bit-rotate message schedule, little-endian I/O, zero padding.

Container

docker run --rm ghcr.io/jaenster/d2-clientless <host> D2XP 1.14.3.71 --login acct:pass
docker run --rm ghcr.io/jaenster/d2-clientless bnftp --head <host> D2XP CheckRevision.mpq

Multi-arch (linux/amd64 + arm64), static musl on scratch. This is also how to run it on Windows or macOS (Docker Desktop / WSL2), since there's no native binary for those yet.

CI

zig build + zig build test run on every push. The e2e job checks out d2-dedicated-server, builds realmd, and drives a full session against it (auth → create → login → MCP → char → ladder → chat) — see ci/e2e.sh. Game entry needs a real game server (the proprietary Game.exe under wine), so ci/e2e-game.sh runs only on a self-hosted runner with the D2 files.

Notes & credits

  • Talking to live Battle.net sends your CD-keys and credentials — rate limits and ToS are your responsibility. Against your own realm it's just a test client.
  • No keys or credentials are committed; they are arguments. Unit tests use synthetic vectors.
  • The D2GS huffman tables in src/huffman.zig are ported from jaenster/D2PacketBased.
  • Sockets go through libc (std.net was dropped in Zig 0.16); native host target.

About

Clientless Diablo II 1.14d Battle.net client (BNCS/MCP/BNFTP/D2GS) in Zig

Resources

Stars

12 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages