zandronum-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/PROTOCOL.md +51 -0
- package/README.md +118 -0
- package/dist/acs/bytecode.d.ts +6 -0
- package/dist/acs/bytecode.js +68 -0
- package/dist/acs/bytecode.js.map +1 -0
- package/dist/acs/collect.d.ts +7 -0
- package/dist/acs/collect.js +34 -0
- package/dist/acs/collect.js.map +1 -0
- package/dist/acs/symbols.d.ts +31 -0
- package/dist/acs/symbols.js +82 -0
- package/dist/acs/symbols.js.map +1 -0
- package/dist/bridge/framing.d.ts +15 -0
- package/dist/bridge/framing.js +45 -0
- package/dist/bridge/framing.js.map +1 -0
- package/dist/bridge/transport.d.ts +35 -0
- package/dist/bridge/transport.js +132 -0
- package/dist/bridge/transport.js.map +1 -0
- package/dist/commands/builders.d.ts +8 -0
- package/dist/commands/builders.js +60 -0
- package/dist/commands/builders.js.map +1 -0
- package/dist/commands/simple.d.ts +17 -0
- package/dist/commands/simple.js +36 -0
- package/dist/commands/simple.js.map +1 -0
- package/dist/correlation/collector.d.ts +10 -0
- package/dist/correlation/collector.js +16 -0
- package/dist/correlation/collector.js.map +1 -0
- package/dist/correlation/sentinel.d.ts +8 -0
- package/dist/correlation/sentinel.js +17 -0
- package/dist/correlation/sentinel.js.map +1 -0
- package/dist/input/keys.d.ts +14 -0
- package/dist/input/keys.js +39 -0
- package/dist/input/keys.js.map +1 -0
- package/dist/map/binary.d.ts +9 -0
- package/dist/map/binary.js +95 -0
- package/dist/map/binary.js.map +1 -0
- package/dist/map/index.d.ts +23 -0
- package/dist/map/index.js +52 -0
- package/dist/map/index.js.map +1 -0
- package/dist/map/locate.d.ts +12 -0
- package/dist/map/locate.js +28 -0
- package/dist/map/locate.js.map +1 -0
- package/dist/map/udmf.d.ts +15 -0
- package/dist/map/udmf.js +61 -0
- package/dist/map/udmf.js.map +1 -0
- package/dist/map/wad.d.ts +11 -0
- package/dist/map/wad.js +26 -0
- package/dist/map/wad.js.map +1 -0
- package/dist/map/zip.d.ts +13 -0
- package/dist/map/zip.js +56 -0
- package/dist/map/zip.js.map +1 -0
- package/dist/parsers/acsprofile.d.ts +18 -0
- package/dist/parsers/acsprofile.js +50 -0
- package/dist/parsers/acsprofile.js.map +1 -0
- package/dist/parsers/acsvars.d.ts +7 -0
- package/dist/parsers/acsvars.js +15 -0
- package/dist/parsers/acsvars.js.map +1 -0
- package/dist/parsers/actorstate.d.ts +35 -0
- package/dist/parsers/actorstate.js +66 -0
- package/dist/parsers/actorstate.js.map +1 -0
- package/dist/parsers/dumpactors.d.ts +15 -0
- package/dist/parsers/dumpactors.js +36 -0
- package/dist/parsers/dumpactors.js.map +1 -0
- package/dist/parsers/hud.d.ts +24 -0
- package/dist/parsers/hud.js +29 -0
- package/dist/parsers/hud.js.map +1 -0
- package/dist/parsers/mapvars.d.ts +10 -0
- package/dist/parsers/mapvars.js +38 -0
- package/dist/parsers/mapvars.js.map +1 -0
- package/dist/parsers/playerstate.d.ts +18 -0
- package/dist/parsers/playerstate.js +35 -0
- package/dist/parsers/playerstate.js.map +1 -0
- package/dist/parsers/renderinfo.d.ts +9 -0
- package/dist/parsers/renderinfo.js +32 -0
- package/dist/parsers/renderinfo.js.map +1 -0
- package/dist/parsers/scripts.d.ts +17 -0
- package/dist/parsers/scripts.js +34 -0
- package/dist/parsers/scripts.js.map +1 -0
- package/dist/parsers/scriptstat.d.ts +14 -0
- package/dist/parsers/scriptstat.js +33 -0
- package/dist/parsers/scriptstat.js.map +1 -0
- package/dist/process/launch.d.ts +17 -0
- package/dist/process/launch.js +47 -0
- package/dist/process/launch.js.map +1 -0
- package/dist/process/registry.d.ts +37 -0
- package/dist/process/registry.js +60 -0
- package/dist/process/registry.js.map +1 -0
- package/dist/saves/png.d.ts +12 -0
- package/dist/saves/png.js +39 -0
- package/dist/saves/png.js.map +1 -0
- package/dist/screenshot/capture.d.ts +30 -0
- package/dist/screenshot/capture.js +46 -0
- package/dist/screenshot/capture.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +775 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/engine-bridge/INPUT.md +51 -0
- package/engine-bridge/anchors.md +29 -0
- package/engine-bridge/apply-bridge.core.mjs +200 -0
- package/engine-bridge/apply-bridge.mjs +26 -0
- package/engine-bridge/overlay/mcp_acsvars.inc +51 -0
- package/engine-bridge/overlay/mcp_actorstate.cpp +75 -0
- package/engine-bridge/overlay/mcp_bridge.cpp +315 -0
- package/engine-bridge/overlay/mcp_bridge.h +27 -0
- package/engine-bridge/overlay/mcp_event.cpp +26 -0
- package/engine-bridge/overlay/mcp_hud.cpp +79 -0
- package/engine-bridge/overlay/mcp_hud.h +16 -0
- package/engine-bridge/overlay/mcp_mapvars.inc +69 -0
- package/engine-bridge/overlay/mcp_renderinfo.cpp +59 -0
- package/engine-bridge/overlay/mcp_scripts.inc +47 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PROTOCOL.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Bridge protocol (v1)
|
|
2
|
+
|
|
3
|
+
The single source of truth for the wire contract between the **engine bridge**
|
|
4
|
+
(`engine-bridge/overlay/mcp_bridge.cpp`) and the **MCP server** (`src/`).
|
|
5
|
+
|
|
6
|
+
- **Transport:** loopback TCP, `127.0.0.1` only. Instance _N_ listens on
|
|
7
|
+
`ZANDRONUM_BRIDGE_PORT + (N - 1)`.
|
|
8
|
+
- **Framing:** newline-delimited JSON (NDJSON). One JSON object per line.
|
|
9
|
+
- **Versioning:** every message carries `"v"`. The client checks the `hello`
|
|
10
|
+
version and refuses to proceed on mismatch. Bump `v` ONLY when this contract
|
|
11
|
+
changes (independent of npm semver and of the engine tag).
|
|
12
|
+
|
|
13
|
+
## Messages
|
|
14
|
+
|
|
15
|
+
### engine → MCP
|
|
16
|
+
|
|
17
|
+
`hello` — sent once, immediately on connect. `caps` lists what the bridge
|
|
18
|
+
supports so the client can degrade gracefully:
|
|
19
|
+
```json
|
|
20
|
+
{"v":1,"t":"hello","engine":"zandronum","bridge":"0.2.0","caps":["cmd","event"]}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`out` — one line of console output, streamed asynchronously:
|
|
24
|
+
```json
|
|
25
|
+
{"v":1,"t":"out","level":0,"text":"Unknown actor 'DoomImp'"}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### MCP → engine
|
|
29
|
+
|
|
30
|
+
`cmd` — a console command to execute:
|
|
31
|
+
```json
|
|
32
|
+
{"v":1,"t":"cmd","text":"summon DoomImp ; echo __MCPDONE_a1__"}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`event` — a raw input event posted via `D_PostEvent` (requires the `event` cap).
|
|
36
|
+
Fire-and-forget; the engine sends no reply. Menus read GUI key events
|
|
37
|
+
(`evtype=4` EV_GUI_Event, `subtype=1` EV_GUI_KeyDown, `data1=GK_*`):
|
|
38
|
+
```json
|
|
39
|
+
{"v":1,"t":"event","evtype":4,"subtype":1,"data1":10,"data2":0}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Correlation
|
|
43
|
+
|
|
44
|
+
The bridge is intentionally dumb — it does not match output to commands. The MCP
|
|
45
|
+
server appends `; echo <sentinel>` to each command, then collects every `out`
|
|
46
|
+
line until it sees the sentinel echoed back. Because console output is a single
|
|
47
|
+
shared stream, **commands must be issued serially per instance.**
|
|
48
|
+
|
|
49
|
+
The bridge's only job: feed `text` to `AddCommandString`, and mirror every
|
|
50
|
+
printed line back as an `out` message. Everything else is the server's problem
|
|
51
|
+
(which is exactly why almost all of the code — and the tests — live in TS).
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/zandromcp.png" alt="zandronum-mcp" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# zandronum-mcp
|
|
6
|
+
|
|
7
|
+
Let an AI assistant supercharge Zandronum development from your editor: Write C++ code, ACS, DECORATE, fix bugs, the works.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
**Easiest:** grab a prebuilt, bridge-patched engine from
|
|
13
|
+
[Releases](https://github.com/rc4l/ZandronumMCP/releases) (Windows for now), set
|
|
14
|
+
`ZANDRONUM_EXE` to it, and add the server with `npx` (see "Add it to your client").
|
|
15
|
+
No source build.
|
|
16
|
+
|
|
17
|
+
To build it yourself — one-time, in order (redo only when you update Zandronum):
|
|
18
|
+
|
|
19
|
+
1. You: Clone the Zandronum source.
|
|
20
|
+
2. You: Compile and build Zandronum.
|
|
21
|
+
3. You: Build this server — `npm run build` (needs Node 20+).
|
|
22
|
+
4. You: Point your MCP client at it (see below).
|
|
23
|
+
5. Your AI Agent: applies the bridge patch and rebuilds the engine.
|
|
24
|
+
6. You: Tell your agent to launch the game and start working.
|
|
25
|
+
|
|
26
|
+
## Patch Zandronum
|
|
27
|
+
|
|
28
|
+
Point the patch script at your Zandronum source tree:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node engine-bridge/apply-bridge.mjs --src path/to/zandronum
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
It copies the overlay files in and adds a few one-line, idempotent hooks; re-running
|
|
35
|
+
skips anything already applied. `--revert` removes the overlay files.
|
|
36
|
+
|
|
37
|
+
## Build Zandronum
|
|
38
|
+
|
|
39
|
+
Compiling Zandronum is Zandronum's own build, not something this repo provides — the
|
|
40
|
+
patch just adds source files to its existing CMake build. If you've never built it,
|
|
41
|
+
start from the official guides at <https://wiki.zandronum.com/> ("Compiling
|
|
42
|
+
Zandronum") to get the toolchain set up.
|
|
43
|
+
|
|
44
|
+
Then build as usual:
|
|
45
|
+
|
|
46
|
+
- Windows: however you normally build it.
|
|
47
|
+
- Linux / macOS: `cmake -B build -DCMAKE_BUILD_TYPE=Release . && cmake --build build`
|
|
48
|
+
from the source tree.
|
|
49
|
+
|
|
50
|
+
The bridge builds on all three (Winsock on Windows, BSD sockets elsewhere).
|
|
51
|
+
|
|
52
|
+
## Build this server
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install
|
|
56
|
+
npm run build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
That creates `dist/server.js`, which is what your MCP client runs.
|
|
60
|
+
|
|
61
|
+
## Add it to your client
|
|
62
|
+
|
|
63
|
+
Most clients (Claude Code, Claude Desktop, Cursor, ...) take a JSON block. Using the
|
|
64
|
+
published package (no clone needed):
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"zandronum": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "zandronum-mcp"],
|
|
72
|
+
"env": {
|
|
73
|
+
"ZANDRONUM_BRIDGE_HOST": "127.0.0.1",
|
|
74
|
+
"ZANDRONUM_BRIDGE_PORT": "7777",
|
|
75
|
+
"ZANDRONUM_EXE": "C:/path/to/zandronum.exe"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Claude Code one-liner: `claude mcp add zandronum -- npx -y zandronum-mcp`.
|
|
83
|
+
|
|
84
|
+
From a local build instead? Use `"command": "node"` with the path to your
|
|
85
|
+
`dist/server.js`. Either way, set `ZANDRONUM_EXE` to the patched binary if you want
|
|
86
|
+
the assistant to launch the game itself. Restart the client to pick up the server.
|
|
87
|
+
|
|
88
|
+
## Run
|
|
89
|
+
|
|
90
|
+
Launch the patched build with the bridge on:
|
|
91
|
+
|
|
92
|
+
```powershell
|
|
93
|
+
# Windows
|
|
94
|
+
$env:ZANDRONUM_BRIDGE_PORT = "7777"; ./zandronum.exe -iwad freedoom2.wad
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Linux / macOS
|
|
99
|
+
ZANDRONUM_BRIDGE_PORT=7777 ./zandronum -iwad freedoom2.wad
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The bridge only starts when that variable is set, so a normal launch is unaffected.
|
|
103
|
+
Instance 1 uses 7777, instance 2 uses 7778, and so on.
|
|
104
|
+
|
|
105
|
+
Then ask the assistant to do things. Some of the tools:
|
|
106
|
+
|
|
107
|
+
- run_command — run any console command
|
|
108
|
+
- list_actor_classes — list the actors the game knows about
|
|
109
|
+
- summon — spawn an actor
|
|
110
|
+
- give — give yourself an item
|
|
111
|
+
- load_map — load a map by name (MAP01, E1M1, ...)
|
|
112
|
+
|
|
113
|
+
## Dev
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npm test
|
|
117
|
+
npm run coverage
|
|
118
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Read function and named-script names out of a compiled ACS BEHAVIOR lump, for
|
|
2
|
+
// mods that ship bytecode but no source. Ports the engine's chunk logic
|
|
3
|
+
// (FBehavior load + FindChunk, p_acs.cpp): FNAM = function names, SNAM = named
|
|
4
|
+
// scripts. Numbered scripts have no names in bytecode (their source #define
|
|
5
|
+
// aliases are compiled away) — only the source indexer can recover those.
|
|
6
|
+
const MAGIC_ACS0 = 0x00534341; // "ACS\0"
|
|
7
|
+
const MAGIC_ACSE = 0x45534341; // "ACSE"
|
|
8
|
+
const MAGIC_ACSe = 0x65534341; // "ACSe"
|
|
9
|
+
const ID_FNAM = 0x4d414e46; // "FNAM"
|
|
10
|
+
const ID_SNAM = 0x4d414e53; // "SNAM"
|
|
11
|
+
/** Where the chunk region lives + how far it runs. null if not chunked bytecode. */
|
|
12
|
+
function locateChunks(lump) {
|
|
13
|
+
if (lump.length < 8)
|
|
14
|
+
return null;
|
|
15
|
+
const magic = lump.readUInt32LE(0);
|
|
16
|
+
if (magic === MAGIC_ACSE || magic === MAGIC_ACSe) {
|
|
17
|
+
return { offset: lump.readUInt32LE(4), end: lump.length };
|
|
18
|
+
}
|
|
19
|
+
if (magic === MAGIC_ACS0) {
|
|
20
|
+
// Old header may embed enhanced chunks: two DWORDs sit just before the
|
|
21
|
+
// directory — [chunks offset][pretag]. (p_acs.cpp:2407-2419)
|
|
22
|
+
const dirofs = lump.readUInt32LE(4);
|
|
23
|
+
if (dirofs >= 24 && dirofs <= lump.length) {
|
|
24
|
+
const pretag = lump.readUInt32LE(dirofs - 4);
|
|
25
|
+
if (pretag === MAGIC_ACSE || pretag === MAGIC_ACSe) {
|
|
26
|
+
return { offset: lump.readUInt32LE(dirofs - 8), end: dirofs - 8 };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/** Walk the chunk list ([id][size][data]) for a chunk id; -1 if absent. */
|
|
33
|
+
function findChunk(lump, region, id) {
|
|
34
|
+
let o = region.offset;
|
|
35
|
+
while (o + 8 <= region.end) {
|
|
36
|
+
if (lump.readUInt32LE(o) === id)
|
|
37
|
+
return o;
|
|
38
|
+
o += lump.readUInt32LE(o + 4) + 8;
|
|
39
|
+
}
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
/** A name chunk: [count][offset×count][nul-terminated strings], offsets relative to chunk+8. */
|
|
43
|
+
function readNameChunk(lump, chunkStart) {
|
|
44
|
+
const base = chunkStart + 8;
|
|
45
|
+
const count = lump.readUInt32LE(base);
|
|
46
|
+
const names = [];
|
|
47
|
+
for (let i = 0; i < count; i++) {
|
|
48
|
+
const start = base + lump.readUInt32LE(base + 4 + i * 4);
|
|
49
|
+
let end = start;
|
|
50
|
+
while (end < lump.length && lump[end] !== 0)
|
|
51
|
+
end++;
|
|
52
|
+
names.push(lump.toString("latin1", start, end));
|
|
53
|
+
}
|
|
54
|
+
return names;
|
|
55
|
+
}
|
|
56
|
+
/** Extract function + named-script names from a BEHAVIOR lump buffer. */
|
|
57
|
+
export function parseBehaviorNames(lump) {
|
|
58
|
+
const region = locateChunks(lump);
|
|
59
|
+
if (!region)
|
|
60
|
+
return { functions: [], namedScripts: [] };
|
|
61
|
+
const fnam = findChunk(lump, region, ID_FNAM);
|
|
62
|
+
const snam = findChunk(lump, region, ID_SNAM);
|
|
63
|
+
return {
|
|
64
|
+
functions: fnam >= 0 ? readNameChunk(lump, fnam) : [],
|
|
65
|
+
namedScripts: snam >= 0 ? readNameChunk(lump, snam) : [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=bytecode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bytecode.js","sourceRoot":"","sources":["../../src/acs/bytecode.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect every compiled ACS bytecode lump in a WAD or PK3: compiled `#library`
|
|
3
|
+
* lumps (a zip entry that is itself ACS bytecode) plus the BEHAVIOR lump of each
|
|
4
|
+
* contained map WAD. This is what lets bytecode name-reading work on a
|
|
5
|
+
* source-less mod, where the scripts live in a compiled library.
|
|
6
|
+
*/
|
|
7
|
+
export declare function collectAcsLumps(buffer: Buffer): Buffer[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { parseWad } from "../map/wad.js";
|
|
2
|
+
import { isZip, listZipEntries, readZipEntry } from "../map/zip.js";
|
|
3
|
+
// ACS bytecode magics: "ACS\0", "ACSE", "ACSe".
|
|
4
|
+
const ACS_MAGICS = new Set([0x00534341, 0x45534341, 0x65534341]);
|
|
5
|
+
function looksLikeAcs(buf) {
|
|
6
|
+
return buf.length >= 4 && ACS_MAGICS.has(buf.readUInt32LE(0));
|
|
7
|
+
}
|
|
8
|
+
function behaviorLumps(wadBuf) {
|
|
9
|
+
return parseWad(wadBuf)
|
|
10
|
+
.lumps.filter((l) => l.name === "BEHAVIOR" && l.size > 0)
|
|
11
|
+
.map((l) => wadBuf.subarray(l.offset, l.offset + l.size));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Collect every compiled ACS bytecode lump in a WAD or PK3: compiled `#library`
|
|
15
|
+
* lumps (a zip entry that is itself ACS bytecode) plus the BEHAVIOR lump of each
|
|
16
|
+
* contained map WAD. This is what lets bytecode name-reading work on a
|
|
17
|
+
* source-less mod, where the scripts live in a compiled library.
|
|
18
|
+
*/
|
|
19
|
+
export function collectAcsLumps(buffer) {
|
|
20
|
+
if (looksLikeAcs(buffer))
|
|
21
|
+
return [buffer]; // a raw compiled .o / BEHAVIOR file
|
|
22
|
+
if (!isZip(buffer))
|
|
23
|
+
return behaviorLumps(buffer);
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const entry of listZipEntries(buffer)) {
|
|
26
|
+
const data = readZipEntry(buffer, entry);
|
|
27
|
+
if (looksLikeAcs(data))
|
|
28
|
+
out.push(data);
|
|
29
|
+
else if (entry.name.toLowerCase().endsWith(".wad"))
|
|
30
|
+
out.push(...behaviorLumps(data));
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=collect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect.js","sourceRoot":"","sources":["../../src/acs/collect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEpE,gDAAgD;AAChD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;AAEjE,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,aAAa,CAAC,MAAc;IACnC,OAAO,QAAQ,CAAC,MAAM,CAAC;SACpB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;SACxD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,IAAI,YAAY,CAAC,MAAM,CAAC;QAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,oCAAoC;IAC/E,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QAAE,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,KAAK,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACzC,IAAI,YAAY,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aAClC,IAAI,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface AcsSymbol {
|
|
2
|
+
kind: "script" | "function";
|
|
3
|
+
name: string | null;
|
|
4
|
+
number: number | null;
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
/** Script type keyword (OPEN/ENTER/...), if present. */
|
|
8
|
+
type: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface AcsIndex {
|
|
11
|
+
symbols: AcsSymbol[];
|
|
12
|
+
defines: Record<string, number>;
|
|
13
|
+
}
|
|
14
|
+
/** Parse `#define` / `#libdefine NAME <int>` lines into a name→number map. */
|
|
15
|
+
export declare function parseDefines(text: string): Record<string, number>;
|
|
16
|
+
interface RawDecl {
|
|
17
|
+
kind: "script" | "function";
|
|
18
|
+
ref: string;
|
|
19
|
+
type: string | null;
|
|
20
|
+
line: number;
|
|
21
|
+
}
|
|
22
|
+
/** Parse `script <ref> [type]` and `function <ret> <name>(` declarations. */
|
|
23
|
+
export declare function parseDeclarations(text: string): RawDecl[];
|
|
24
|
+
/** Build a symbol index across files, resolving script names ↔ numbers via defines. */
|
|
25
|
+
export declare function buildIndex(files: Array<{
|
|
26
|
+
path: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}>): AcsIndex;
|
|
29
|
+
/** Find symbols by script number or by name (case-insensitive). */
|
|
30
|
+
export declare function findSymbol(index: AcsIndex, ref: string): AcsSymbol[];
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Static ACS source indexer: parse a mod's .acs files into a symbol index so a
|
|
2
|
+
// profiler "script 1017" can be resolved to its name + file:line. Pure — the
|
|
3
|
+
// server reads the files and feeds (path, text) pairs in.
|
|
4
|
+
const DEFINE_RE = /^\s*#(?:lib)?define\s+(\w+)\s+(-?\d+)\b/;
|
|
5
|
+
const SCRIPT_RE = /^\s*script\s+("[^"]*"|-?\d+|[A-Za-z_]\w*)\s*([A-Za-z_]\w*)?/i;
|
|
6
|
+
const FUNCTION_RE = /^\s*function\s+\w+\s+([A-Za-z_]\w*)\s*\(/i;
|
|
7
|
+
function baseName(path) {
|
|
8
|
+
const parts = path.split(/[/\\]/);
|
|
9
|
+
return parts[parts.length - 1];
|
|
10
|
+
}
|
|
11
|
+
/** Parse `#define` / `#libdefine NAME <int>` lines into a name→number map. */
|
|
12
|
+
export function parseDefines(text) {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const line of text.split(/\r?\n/)) {
|
|
15
|
+
const m = line.match(DEFINE_RE);
|
|
16
|
+
if (m)
|
|
17
|
+
out[m[1]] = Number(m[2]);
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
/** Parse `script <ref> [type]` and `function <ret> <name>(` declarations. */
|
|
22
|
+
export function parseDeclarations(text) {
|
|
23
|
+
const out = [];
|
|
24
|
+
const lines = text.split(/\r?\n/);
|
|
25
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26
|
+
const sm = lines[i].match(SCRIPT_RE);
|
|
27
|
+
if (sm) {
|
|
28
|
+
out.push({ kind: "script", ref: sm[1], type: sm[2] ?? null, line: i + 1 });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const fm = lines[i].match(FUNCTION_RE);
|
|
32
|
+
if (fm)
|
|
33
|
+
out.push({ kind: "function", ref: fm[1], type: null, line: i + 1 });
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** Build a symbol index across files, resolving script names ↔ numbers via defines. */
|
|
38
|
+
export function buildIndex(files) {
|
|
39
|
+
const defines = {};
|
|
40
|
+
for (const f of files)
|
|
41
|
+
Object.assign(defines, parseDefines(f.text));
|
|
42
|
+
// ACS identifiers are case-insensitive, so resolve define refs that way.
|
|
43
|
+
const definesLower = {};
|
|
44
|
+
const reverse = {};
|
|
45
|
+
for (const [name, num] of Object.entries(defines)) {
|
|
46
|
+
definesLower[name.toLowerCase()] = num;
|
|
47
|
+
if (!(num in reverse))
|
|
48
|
+
reverse[num] = name;
|
|
49
|
+
}
|
|
50
|
+
const symbols = [];
|
|
51
|
+
for (const f of files) {
|
|
52
|
+
const file = baseName(f.path);
|
|
53
|
+
for (const d of parseDeclarations(f.text)) {
|
|
54
|
+
if (d.kind === "function") {
|
|
55
|
+
symbols.push({ kind: "function", name: d.ref, number: null, file, line: d.line, type: null });
|
|
56
|
+
}
|
|
57
|
+
else if (d.ref.startsWith('"')) {
|
|
58
|
+
symbols.push({ kind: "script", name: d.ref.slice(1, -1), number: null, file, line: d.line, type: d.type });
|
|
59
|
+
}
|
|
60
|
+
else if (/^-?\d+$/.test(d.ref)) {
|
|
61
|
+
const n = Number(d.ref);
|
|
62
|
+
symbols.push({ kind: "script", name: reverse[n] ?? null, number: n, file, line: d.line, type: d.type });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const key = d.ref.toLowerCase();
|
|
66
|
+
const n = key in definesLower ? definesLower[key] : null;
|
|
67
|
+
symbols.push({ kind: "script", name: d.ref, number: n, file, line: d.line, type: d.type });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { symbols, defines };
|
|
72
|
+
}
|
|
73
|
+
/** Find symbols by script number or by name (case-insensitive). */
|
|
74
|
+
export function findSymbol(index, ref) {
|
|
75
|
+
if (/^-?\d+$/.test(ref)) {
|
|
76
|
+
const n = Number(ref);
|
|
77
|
+
return index.symbols.filter((s) => s.number === n);
|
|
78
|
+
}
|
|
79
|
+
const lower = ref.toLowerCase();
|
|
80
|
+
return index.symbols.filter((s) => s.name !== null && s.name.toLowerCase() === lower);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=symbols.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"symbols.js","sourceRoot":"","sources":["../../src/acs/symbols.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BridgeMessage } from "../types.js";
|
|
2
|
+
/** Serialize a message to a single NDJSON line (newline-terminated). */
|
|
3
|
+
export declare function encodeMessage(msg: BridgeMessage): string;
|
|
4
|
+
/**
|
|
5
|
+
* Streaming NDJSON decoder. Feed it raw socket chunks; it returns whole
|
|
6
|
+
* messages and buffers any trailing partial line until the rest arrives.
|
|
7
|
+
*
|
|
8
|
+
* Pure and I/O-free — this is the most-tested unit in the bridge layer.
|
|
9
|
+
*/
|
|
10
|
+
export declare class NdjsonDecoder {
|
|
11
|
+
private buffer;
|
|
12
|
+
push(chunk: string): BridgeMessage[];
|
|
13
|
+
/** Bytes buffered but not yet terminated by a newline. */
|
|
14
|
+
get pending(): number;
|
|
15
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Serialize a message to a single NDJSON line (newline-terminated). */
|
|
2
|
+
export function encodeMessage(msg) {
|
|
3
|
+
return JSON.stringify(msg) + "\n";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Streaming NDJSON decoder. Feed it raw socket chunks; it returns whole
|
|
7
|
+
* messages and buffers any trailing partial line until the rest arrives.
|
|
8
|
+
*
|
|
9
|
+
* Pure and I/O-free — this is the most-tested unit in the bridge layer.
|
|
10
|
+
*/
|
|
11
|
+
export class NdjsonDecoder {
|
|
12
|
+
buffer = "";
|
|
13
|
+
push(chunk) {
|
|
14
|
+
this.buffer += chunk;
|
|
15
|
+
const messages = [];
|
|
16
|
+
let newlineIndex;
|
|
17
|
+
while ((newlineIndex = this.buffer.indexOf("\n")) !== -1) {
|
|
18
|
+
const line = this.buffer.slice(0, newlineIndex).trim();
|
|
19
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
20
|
+
if (line.length === 0)
|
|
21
|
+
continue;
|
|
22
|
+
const parsed = tryParse(line);
|
|
23
|
+
if (parsed)
|
|
24
|
+
messages.push(parsed);
|
|
25
|
+
}
|
|
26
|
+
return messages;
|
|
27
|
+
}
|
|
28
|
+
/** Bytes buffered but not yet terminated by a newline. */
|
|
29
|
+
get pending() {
|
|
30
|
+
return this.buffer.length;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function tryParse(line) {
|
|
34
|
+
try {
|
|
35
|
+
const obj = JSON.parse(line);
|
|
36
|
+
if (obj && typeof obj === "object" && typeof obj.t === "string") {
|
|
37
|
+
return obj;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=framing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framing.js","sourceRoot":"","sources":["../../src/bridge/framing.ts"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,MAAM,UAAU,aAAa,CAAC,GAAkB;IAC9C,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,aAAa;IAChB,MAAM,GAAG,EAAE,CAAC;IAEpB,IAAI,CAAC,KAAa;QAChB,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;QACrB,MAAM,QAAQ,GAAoB,EAAE,CAAC;QACrC,IAAI,YAAoB,CAAC;QACzB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACzD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC;YACvD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC;YAClD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAChC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,MAAM;gBAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,0DAA0D;IAC1D,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC5B,CAAC;CACF;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,CAAC;QACH,MAAM,GAAG,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAQ,GAAqB,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;YACnF,OAAO,GAAoB,CAAC;QAC9B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { HelloMessage } from "../types.js";
|
|
3
|
+
export interface BridgeClientOptions {
|
|
4
|
+
host?: string;
|
|
5
|
+
port: number;
|
|
6
|
+
commandTimeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Talks to one bridge-patched Zandronum instance over loopback TCP.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: console output is a single shared stream, so commands are correlated by
|
|
12
|
+
* a unique echo sentinel and MUST be issued serially per instance. Callers
|
|
13
|
+
* should await each runCommand before sending the next.
|
|
14
|
+
*/
|
|
15
|
+
export declare class BridgeClient extends EventEmitter {
|
|
16
|
+
private socket?;
|
|
17
|
+
private readonly decoder;
|
|
18
|
+
private pending;
|
|
19
|
+
private capsSet;
|
|
20
|
+
private seq;
|
|
21
|
+
private readonly host;
|
|
22
|
+
private readonly port;
|
|
23
|
+
private readonly commandTimeoutMs;
|
|
24
|
+
constructor(opts: BridgeClientOptions);
|
|
25
|
+
connect(): Promise<HelloMessage>;
|
|
26
|
+
private dispatch;
|
|
27
|
+
private checkPending;
|
|
28
|
+
runCommand(text: string): Promise<string[]>;
|
|
29
|
+
/** Whether the connected bridge advertised a capability in its hello. */
|
|
30
|
+
supports(cap: string): boolean;
|
|
31
|
+
/** Post a raw input event to the engine (fire-and-forget; no reply). */
|
|
32
|
+
sendEvent(evtype: number, subtype: number, data1: number, data2?: number): void;
|
|
33
|
+
private failAllPending;
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|