A small, strictly-typed Node client for the DFHack
Remote RPC protocol — protobuf over raw TCP (127.0.0.1:5000). It lets a Node
process read from (and, later, act on) a live Dwarf Fortress fort without
compiling against DFHack's C++.
This is a TypeScript port of the original browser client
(alexchandel/dfhack-remote, ISC).
The protocol codec is carried over; the transport was rewritten from
WebSocket + websockify to Node net.Socket, RunCommand console output is now
captured instead of discarded, and methods bind lazily on first use.
Built primarily as the RPC layer for a DFHack MCP server.
npm install dfhack-remote-nodeThe published package ships prebuilt (dist/ ESM + type declarations, proto
bundle inlined), so no build step is needed to consume it.
To work on the library from a clone instead:
npm install
npm run build # regenerates build/proto.json and bundles dist/build/proto.json (the compiled protobuf bundle) is committed, so the source
runs immediately; npm run build produces the distributable dist/ (ESM +
type declarations, with the proto bundle inlined).
import { DwarfClient } from 'dfhack-remote-node';
const df = new DwarfClient(); // defaults to 127.0.0.1:5000
await df.connect(); // handshake
console.log(await df.getVersion()); // DFHack version string
const world = await df.getWorldInfo(); // decoded GetWorldInfoOut
console.log(world.worldName?.englishName);
// Arbitrary Lua snippet; returns whatever it prints (captured console output):
const text = await df.runLuaSnippet('print(#df.global.world.units.active)');
// Any method in the METHODS table, by name:
const map = await df.call('GetMapInfo');
df.close();Every reply object also carries _text — the concatenated console output from
any TEXT frames the call produced.
src/ is split by concern (all TypeScript ESM):
| File | Responsibility |
|---|---|
codec.ts |
wire framing: magic bytes, frame ids, encodeMessage, readHandshake, readFrame |
errors.ts |
RpcError, ProtocolError |
connection.ts |
DfConnection — the net.Socket transport + call queue |
methods.ts |
the METHODS table (fully-qualified protobuf type names) |
proto.ts |
loads the committed proto bundle as a JSON module import |
client.ts |
DwarfClient — the high-level API |
index.ts |
public exports |
The source imports the proto bundle as a JSON module
(import protoJson from '../build/proto.json' with { type: 'json' }), so the
bundler inlines it into dist/ — there is no runtime file lookup.
The
.protofiles are pinned to DFHack53.15-r2(pulled from DFHack/dfhack at the commit the running build reports viagetGitDescription). To retarget another version, replace the files from the matching tag and re-runnpm run gen-proto. Method input/output types inMETHODS(src/methods.ts) are fully-qualified names that must match DFHack's registration, orBindMethodreports a "wrong signature" at runtime — which makes drift detectable per method.
npm run build # gen-proto + tsup -> dist/ (ESM + .d.ts, proto inlined)
npm run gen-proto # regenerate build/proto.json from proto/*.proto
npm run typecheck # tsc --noEmit
npm run lint # eslint (flat config)
npm run format # prettier --write
npm test # offline: mock server speaks the real wire format, no game needed
npm run spike # live: connect to a running fort, print version + world + fort namenpm test validates handshake, framing, lazy binding, protobuf round-trip, and
TEXT-frame capture against an in-process mock. npm run spike (M0) requires
Dwarf Fortress running with DFHack and a fort loaded.
- Handshake: client sends
DFHack?\n+i32(1), server repliesDFHack!\n+i32(1). - Message header:
id:i16, pad:u16, size:i32(little-endian), thensizebody bytes. - A call's reply is zero or more
TEXTframes (console output) followed by oneRESULTframe (reply protobuf) or oneFAILframe (errno in the size field). - Calls are serialized — DFHack handles one at a time per socket.
GetBlockListonly returns blocks changed since the last call — don't build a full-map snapshot on it naively (useResetMapHashesto force a resend).
Issues and PRs welcome. Please keep the module boundaries above intact, run
npm run typecheck, npm run lint, and npm test before opening a PR, and add
new RPC methods to src/methods.ts with their fully-qualified protobuf type
names. Changes touching the live protocol should be verified against a running
fort with npm run spike.
ISC. Ported from alexchandel/dfhack-remote (Copyright © 2020 Alexander Chandel); the original copyright and license are preserved in LICENSE.md.