enfusion-mcp 0.5.0 → 0.6.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/README.md +194 -188
- package/dist/pak/reader.d.ts +28 -0
- package/dist/pak/reader.d.ts.map +1 -0
- package/dist/pak/reader.js +141 -0
- package/dist/pak/reader.js.map +1 -0
- package/dist/pak/vfs.d.ts +56 -0
- package/dist/pak/vfs.d.ts.map +1 -0
- package/dist/pak/vfs.js +200 -0
- package/dist/pak/vfs.js.map +1 -0
- package/dist/prompts/create-mod.d.ts.map +1 -1
- package/dist/prompts/create-mod.js +151 -139
- package/dist/prompts/create-mod.js.map +1 -1
- package/dist/prompts/modify-mod.d.ts.map +1 -1
- package/dist/prompts/modify-mod.js +72 -70
- package/dist/prompts/modify-mod.js.map +1 -1
- package/dist/tools/asset-search.d.ts.map +1 -1
- package/dist/tools/asset-search.js +28 -8
- package/dist/tools/asset-search.js.map +1 -1
- package/dist/tools/game-browse.d.ts.map +1 -1
- package/dist/tools/game-browse.js +39 -3
- package/dist/tools/game-browse.js.map +1 -1
- package/dist/tools/game-read.d.ts.map +1 -1
- package/dist/tools/game-read.js +67 -30
- package/dist/tools/game-read.js.map +1 -1
- package/dist/tools/wb-entities.d.ts.map +1 -1
- package/dist/tools/wb-entities.js +33 -6
- package/dist/tools/wb-entities.js.map +1 -1
- package/dist/tools/wiki-search.d.ts.map +1 -1
- package/dist/tools/wiki-search.js +2 -1
- package/dist/tools/wiki-search.js.map +1 -1
- package/dist/workbench/client.d.ts +9 -3
- package/dist/workbench/client.d.ts.map +1 -1
- package/dist/workbench/client.js +27 -16
- package/dist/workbench/client.js.map +1 -1
- package/mod/Scripts/WorkbenchGame/EnfusionMCP/EMCP_WB_ModifyEntity.c +472 -295
- package/package.json +58 -58
package/README.md
CHANGED
|
@@ -1,188 +1,194 @@
|
|
|
1
|
-
# enfusion-mcp
|
|
2
|
-
|
|
3
|
-
MCP server for Arma Reforger modding. Describe what you want to build, and Claude handles everything — API research, code generation, project scaffolding, Workbench control, and in-editor testing. Zero modding experience required.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
### Claude Code (Windows)
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
claude mcp add --scope user enfusion-mcp -- cmd /c npx -y enfusion-mcp
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
### Claude Code (macOS / Linux)
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
claude mcp add --scope user enfusion-mcp -- npx -y enfusion-mcp
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Restart Claude Code. Verify with `/mcp`.
|
|
20
|
-
|
|
21
|
-
### Claude Desktop
|
|
22
|
-
|
|
23
|
-
Add to your `claude_desktop_config.json`:
|
|
24
|
-
|
|
25
|
-
**Windows:**
|
|
26
|
-
|
|
27
|
-
```json
|
|
28
|
-
{
|
|
29
|
-
"mcpServers": {
|
|
30
|
-
"enfusion-mcp": {
|
|
31
|
-
"command": "cmd",
|
|
32
|
-
"args": ["/c", "npx", "-y", "enfusion-mcp"]
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
**macOS / Linux:**
|
|
39
|
-
|
|
40
|
-
```json
|
|
41
|
-
{
|
|
42
|
-
"mcpServers": {
|
|
43
|
-
"enfusion-mcp": {
|
|
44
|
-
"command": "npx",
|
|
45
|
-
"args": ["-y", "enfusion-mcp"]
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
- *"Create a
|
|
62
|
-
- *"
|
|
63
|
-
- *"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
|
93
|
-
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
1
|
+
# enfusion-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for Arma Reforger modding. Describe what you want to build, and Claude handles everything — API research, code generation, project scaffolding, Workbench control, and in-editor testing. Zero modding experience required.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
### Claude Code (Windows)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
claude mcp add --scope user enfusion-mcp -- cmd /c npx -y enfusion-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Claude Code (macOS / Linux)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
claude mcp add --scope user enfusion-mcp -- npx -y enfusion-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Restart Claude Code. Verify with `/mcp`.
|
|
20
|
+
|
|
21
|
+
### Claude Desktop
|
|
22
|
+
|
|
23
|
+
Add to your `claude_desktop_config.json`:
|
|
24
|
+
|
|
25
|
+
**Windows:**
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"enfusion-mcp": {
|
|
31
|
+
"command": "cmd",
|
|
32
|
+
"args": ["/c", "npx", "-y", "enfusion-mcp"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**macOS / Linux:**
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"enfusion-mcp": {
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["-y", "enfusion-mcp"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Restart Claude Desktop. Verify with `/mcp`.
|
|
52
|
+
|
|
53
|
+
### Workbench Plugin
|
|
54
|
+
|
|
55
|
+
The live Workbench tools (`wb_*`) require handler scripts running inside Workbench. These ship with the package in `mod/Scripts/WorkbenchGame/EnfusionMCP/` and are installed automatically when Claude launches Workbench via `wb_launch`.
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
Just ask Claude to make a mod:
|
|
60
|
+
|
|
61
|
+
- *"Create a HUD widget that shows player health and stamina"*
|
|
62
|
+
- *"Make a zombie survival game mode with wave spawning"*
|
|
63
|
+
- *"Create a custom faction called CSAT with desert camo soldiers"*
|
|
64
|
+
- *"Add an interactive object that heals the player when used"*
|
|
65
|
+
- *"Override the damage system to add armor mechanics"*
|
|
66
|
+
|
|
67
|
+
Or use the guided prompts for structured workflows:
|
|
68
|
+
|
|
69
|
+
| Prompt | Description |
|
|
70
|
+
|--------|-------------|
|
|
71
|
+
| `/create-mod` | Full guided mod creation — from idea to built addon |
|
|
72
|
+
| `/modify-mod` | Modify or extend an existing mod project |
|
|
73
|
+
|
|
74
|
+
Claude will:
|
|
75
|
+
|
|
76
|
+
1. **Assess complexity** — simple mods are built in one pass; large mods (e.g., a DayZ-style overhaul) get broken into phases with a plan you approve before any code is written
|
|
77
|
+
2. **Research** the Enfusion API (8,693 indexed classes), the Arma Reforger wiki (250+ guides), and base game assets (read directly from `.pak` archives) to find the right approach
|
|
78
|
+
3. **Scaffold** the full addon — `.gproj`, scripts, prefabs, configs, UI layouts
|
|
79
|
+
4. **Launch Workbench** if it's not already running
|
|
80
|
+
5. **Load the project**, reload scripts, register resources
|
|
81
|
+
6. **Validate and build** the addon
|
|
82
|
+
7. **Enter play mode** so you can test in-game
|
|
83
|
+
|
|
84
|
+
For complex mods, a `MODPLAN.md` is written to the project root tracking the full vision, completed phases, and what's next — so any future session can pick up right where the last one left off via `/modify-mod`.
|
|
85
|
+
|
|
86
|
+
## Tools
|
|
87
|
+
|
|
88
|
+
### Offline Tools
|
|
89
|
+
|
|
90
|
+
Work without Workbench running — API search, mod scaffolding, code generation, validation, and building.
|
|
91
|
+
|
|
92
|
+
| Tool | What it does |
|
|
93
|
+
|------|-------------|
|
|
94
|
+
| `api_search` | Search 8,693 Enfusion/Arma Reforger API classes and methods |
|
|
95
|
+
| `wiki_search` | Search 250+ tutorials and guides from the Enfusion engine docs and BI Community Wiki |
|
|
96
|
+
| `game_browse` | Browse base game files — loose files and `.pak` archives transparently |
|
|
97
|
+
| `game_read` | Read base game files — scripts, prefabs, configs from loose files or `.pak` |
|
|
98
|
+
| `asset_search` | Search game assets by name across loose files and `.pak` archives |
|
|
99
|
+
| `project_browse` | List files in a mod project directory |
|
|
100
|
+
| `project_read` | Read any project file |
|
|
101
|
+
| `project_write` | Write or update project files |
|
|
102
|
+
| `mod_create` | Scaffold a complete addon with directory structure and `.gproj` |
|
|
103
|
+
| `script_create` | Generate Enforce Script (`.c`) files — 7 types: component, gamemode, action, entity, manager, modded, basic |
|
|
104
|
+
| `prefab_create` | Generate Entity Template (`.et`) prefabs — 7 types: character, vehicle, weapon, spawnpoint, gamemode, interactive, generic |
|
|
105
|
+
| `layout_create` | Generate UI layout (`.layout`) files — 5 types: hud, menu, dialog, list, custom |
|
|
106
|
+
| `config_create` | Generate config files — factions, missions, entity catalogs, editor placeables |
|
|
107
|
+
| `server_config` | Generate dedicated server config for local testing |
|
|
108
|
+
| `mod_validate` | Validate project structure, scripts, prefabs, configs, and naming |
|
|
109
|
+
| `mod_build` | Build the addon using the Workbench CLI |
|
|
110
|
+
|
|
111
|
+
### Live Workbench Tools
|
|
112
|
+
|
|
113
|
+
Control a running Workbench instance over TCP. Requires the handler scripts installed (see setup above).
|
|
114
|
+
|
|
115
|
+
| Tool | What it does |
|
|
116
|
+
|------|-------------|
|
|
117
|
+
| `wb_launch` | Start Workbench if not running, wait for NET API |
|
|
118
|
+
| `wb_connect` | Test connection to Workbench |
|
|
119
|
+
| `wb_state` | Full state snapshot — mode, world, entity count, selection |
|
|
120
|
+
| `wb_play` | Switch to game mode (Play in Editor) |
|
|
121
|
+
| `wb_stop` | Return to edit mode |
|
|
122
|
+
| `wb_save` | Save the current world |
|
|
123
|
+
| `wb_undo_redo` | Undo or redo the last action |
|
|
124
|
+
| `wb_open_resource` | Open a resource in its editor |
|
|
125
|
+
| `wb_reload` | Reload scripts or plugins without restarting |
|
|
126
|
+
| `wb_execute_action` | Run any Workbench menu action by path |
|
|
127
|
+
| `wb_entity_create` | Create entity from prefab at a position |
|
|
128
|
+
| `wb_entity_delete` | Delete entity by name |
|
|
129
|
+
| `wb_entity_list` | List and search entities in the world |
|
|
130
|
+
| `wb_entity_inspect` | Get entity details — properties, components, children |
|
|
131
|
+
| `wb_entity_modify` | Move, rotate, rename, reparent, set/clear/get/list properties |
|
|
132
|
+
| `wb_entity_select` | Select, deselect, clear, get current selection |
|
|
133
|
+
| `wb_component` | Add, remove, list entity components |
|
|
134
|
+
| `wb_terrain` | Query terrain height and world bounds |
|
|
135
|
+
| `wb_layers` | Create, delete, rename layers, set visibility/active |
|
|
136
|
+
| `wb_resources` | Register resources, rebuild database |
|
|
137
|
+
| `wb_prefabs` | Create templates, save, GUID lookup |
|
|
138
|
+
| `wb_clipboard` | Copy, cut, paste, duplicate entities |
|
|
139
|
+
| `wb_script_editor` | Read/write lines in the open script file |
|
|
140
|
+
| `wb_localization` | String table CRUD for localization |
|
|
141
|
+
| `wb_projects` | List loaded projects, open `.gproj` files |
|
|
142
|
+
| `wb_validate` | Material and texture validation |
|
|
143
|
+
|
|
144
|
+
### Mod Patterns
|
|
145
|
+
|
|
146
|
+
10 built-in templates for `mod_create`:
|
|
147
|
+
|
|
148
|
+
`game-mode` `custom-faction` `custom-action` `spawn-system` `custom-component` `modded-behavior` `admin-tool` `custom-vehicle` `weapon-reskin` `hud-widget`
|
|
149
|
+
|
|
150
|
+
### MCP Resources
|
|
151
|
+
|
|
152
|
+
| URI | Description |
|
|
153
|
+
|-----|-------------|
|
|
154
|
+
| `enfusion://class/{className}` | Full class docs with inheritance, methods, ancestors/descendants |
|
|
155
|
+
| `enfusion://pattern/{patternName}` | Mod pattern definition with all templates |
|
|
156
|
+
| `enfusion://group/{groupName}` | API group with class list |
|
|
157
|
+
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
All optional. Sensible defaults are used when nothing is set.
|
|
161
|
+
|
|
162
|
+
| Environment Variable | Description | Default |
|
|
163
|
+
|---------------------|-------------|---------|
|
|
164
|
+
| `ENFUSION_PROJECT_PATH` | Default mod output directory | `~/Documents/My Games/ArmaReforgerWorkbench/addons` |
|
|
165
|
+
| `ENFUSION_WORKBENCH_PATH` | Path to Arma Reforger Tools | `C:\Program Files (x86)\Steam\steamapps\common\Arma Reforger Tools` |
|
|
166
|
+
| `ENFUSION_GAME_PATH` | Path to the Arma Reforger game install (used as CWD when launching Workbench so base-game addons resolve correctly) | Auto-detected from sibling of `ENFUSION_WORKBENCH_PATH` |
|
|
167
|
+
| `ENFUSION_WORKBENCH_HOST` | NET API host | `127.0.0.1` |
|
|
168
|
+
| `ENFUSION_WORKBENCH_PORT` | NET API port | `5775` |
|
|
169
|
+
|
|
170
|
+
Config can also be loaded from `~/.enfusion-mcp/config.json`. Environment variables take priority.
|
|
171
|
+
|
|
172
|
+
## Requirements
|
|
173
|
+
|
|
174
|
+
- **Node.js 20+**
|
|
175
|
+
- **Arma Reforger Tools** (Steam) — needed for `mod_build` and all `wb_*` tools
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
git clone https://github.com/Articulated7/enfusion-mcp.git
|
|
181
|
+
cd enfusion-mcp
|
|
182
|
+
npm install
|
|
183
|
+
npm run scrape # Build API index from Workbench docs
|
|
184
|
+
npm run build
|
|
185
|
+
npm test # 187 tests
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Documentation
|
|
189
|
+
|
|
190
|
+
Full documentation is on the [GitHub Wiki](https://github.com/Articulated7/enfusion-mcp/wiki).
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface PakFileEntry {
|
|
2
|
+
kind: "file";
|
|
3
|
+
name: string;
|
|
4
|
+
/** Byte offset of this file's data within the DATA chunk payload */
|
|
5
|
+
offset: number;
|
|
6
|
+
compressedLen: number;
|
|
7
|
+
decompressedLen: number;
|
|
8
|
+
compressed: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface PakDirEntry {
|
|
11
|
+
kind: "dir";
|
|
12
|
+
name: string;
|
|
13
|
+
children: Map<string, PakDirEntry | PakFileEntry>;
|
|
14
|
+
}
|
|
15
|
+
export interface PakIndex {
|
|
16
|
+
root: PakDirEntry;
|
|
17
|
+
/** Absolute byte position in the .pak file where the DATA payload starts */
|
|
18
|
+
dataStart: number;
|
|
19
|
+
/** Path to the .pak file on disk */
|
|
20
|
+
pakPath: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse a .pak file's metadata without reading the DATA payload.
|
|
24
|
+
* Reads chunk headers to locate the DATA and FILE sections, then parses
|
|
25
|
+
* the FILE chunk's recursive entry tree into an in-memory directory structure.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parsePakIndex(pakPath: string): PakIndex;
|
|
28
|
+
//# sourceMappingURL=reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../src/pak/reader.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,WAAW,CAAC;IAClB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;CACjB;AAYD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CA+DvD"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { openSync, readSync, closeSync, fstatSync } from "node:fs";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
// ── Chunk magic constants ────────────────────────────────────────────────────
|
|
4
|
+
const MAGIC_FORM = 0x464f524d; // "FORM"
|
|
5
|
+
const MAGIC_PAC1 = 0x50414331; // "PAC1"
|
|
6
|
+
const MAGIC_HEAD = 0x48454144; // "HEAD"
|
|
7
|
+
const MAGIC_DATA = 0x44415441; // "DATA"
|
|
8
|
+
const MAGIC_FILE = 0x46494c45; // "FILE"
|
|
9
|
+
// ── Parser ───────────────────────────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Parse a .pak file's metadata without reading the DATA payload.
|
|
12
|
+
* Reads chunk headers to locate the DATA and FILE sections, then parses
|
|
13
|
+
* the FILE chunk's recursive entry tree into an in-memory directory structure.
|
|
14
|
+
*/
|
|
15
|
+
export function parsePakIndex(pakPath) {
|
|
16
|
+
const fd = openSync(pakPath, "r");
|
|
17
|
+
try {
|
|
18
|
+
const fileSize = fstatSync(fd).size;
|
|
19
|
+
// ── FORM chunk ───────────────────────────────────────────────────────
|
|
20
|
+
// Layout: "FORM" (4B) + u32BE size + "PAC1" (4B) = 12 bytes
|
|
21
|
+
const formBuf = readAt(fd, 0, 12);
|
|
22
|
+
const formMagic = formBuf.readUInt32BE(0);
|
|
23
|
+
if (formMagic !== MAGIC_FORM) {
|
|
24
|
+
throw new Error(`Not a PAK file: missing FORM header (got 0x${formMagic.toString(16)})`);
|
|
25
|
+
}
|
|
26
|
+
const pac1 = formBuf.readUInt32BE(8);
|
|
27
|
+
if (pac1 !== MAGIC_PAC1) {
|
|
28
|
+
throw new Error(`Not a PAK file: expected PAC1 identifier (got 0x${pac1.toString(16)})`);
|
|
29
|
+
}
|
|
30
|
+
// ── Walk chunks after FORM header ────────────────────────────────────
|
|
31
|
+
// Chunks start at offset 12 (after FORM header).
|
|
32
|
+
// Each chunk: magic (4B BE) + size (4B BE) + payload (size bytes).
|
|
33
|
+
let pos = 12;
|
|
34
|
+
let dataStart = -1;
|
|
35
|
+
let fileChunkOffset = -1;
|
|
36
|
+
let fileChunkLen = 0;
|
|
37
|
+
while (pos + 8 <= fileSize) {
|
|
38
|
+
const hdr = readAt(fd, pos, 8);
|
|
39
|
+
const magic = hdr.readUInt32BE(0);
|
|
40
|
+
const chunkLen = hdr.readUInt32BE(4);
|
|
41
|
+
if (magic === MAGIC_HEAD) {
|
|
42
|
+
// Skip HEAD chunk entirely
|
|
43
|
+
pos += 8 + chunkLen;
|
|
44
|
+
}
|
|
45
|
+
else if (magic === MAGIC_DATA) {
|
|
46
|
+
// Record where the DATA payload begins (right after its 8-byte header)
|
|
47
|
+
dataStart = pos + 8;
|
|
48
|
+
pos += 8 + chunkLen;
|
|
49
|
+
}
|
|
50
|
+
else if (magic === MAGIC_FILE) {
|
|
51
|
+
fileChunkOffset = pos + 8;
|
|
52
|
+
fileChunkLen = chunkLen;
|
|
53
|
+
break; // FILE is the last chunk we care about
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Unknown chunk — skip it
|
|
57
|
+
logger.debug(`PAK unknown chunk 0x${magic.toString(16)} at offset ${pos}, skipping`);
|
|
58
|
+
pos += 8 + chunkLen;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (dataStart < 0) {
|
|
62
|
+
throw new Error("PAK file missing DATA chunk");
|
|
63
|
+
}
|
|
64
|
+
if (fileChunkOffset < 0) {
|
|
65
|
+
throw new Error("PAK file missing FILE chunk");
|
|
66
|
+
}
|
|
67
|
+
// ── Parse FILE chunk ─────────────────────────────────────────────────
|
|
68
|
+
const fileBuf = readAt(fd, fileChunkOffset, fileChunkLen);
|
|
69
|
+
const root = parseFileTree(fileBuf);
|
|
70
|
+
return { root, dataStart, pakPath };
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
closeSync(fd);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ── FILE tree parser ─────────────────────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* Parse the recursive file entry tree from the FILE chunk payload.
|
|
79
|
+
* Uses an iterative approach with an explicit stack to avoid deep recursion.
|
|
80
|
+
*/
|
|
81
|
+
function parseFileTree(buf) {
|
|
82
|
+
const state = { offset: 0 };
|
|
83
|
+
// The FILE chunk contains a single root entry (always a directory)
|
|
84
|
+
const root = parseEntry(buf, state);
|
|
85
|
+
if (root.kind !== "dir") {
|
|
86
|
+
throw new Error("PAK FILE chunk root entry is not a directory");
|
|
87
|
+
}
|
|
88
|
+
return root;
|
|
89
|
+
}
|
|
90
|
+
function parseEntry(buf, state) {
|
|
91
|
+
const entryKind = buf.readUInt8(state.offset);
|
|
92
|
+
state.offset += 1;
|
|
93
|
+
const nameLen = buf.readUInt8(state.offset);
|
|
94
|
+
state.offset += 1;
|
|
95
|
+
const name = buf.toString("utf8", state.offset, state.offset + nameLen);
|
|
96
|
+
state.offset += nameLen;
|
|
97
|
+
if (entryKind === 0) {
|
|
98
|
+
// Directory
|
|
99
|
+
const childCount = buf.readUInt32LE(state.offset);
|
|
100
|
+
state.offset += 4;
|
|
101
|
+
const children = new Map();
|
|
102
|
+
for (let i = 0; i < childCount; i++) {
|
|
103
|
+
const child = parseEntry(buf, state);
|
|
104
|
+
children.set(child.name, child);
|
|
105
|
+
}
|
|
106
|
+
return { kind: "dir", name, children };
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// File
|
|
110
|
+
const offset = buf.readUInt32LE(state.offset);
|
|
111
|
+
state.offset += 4;
|
|
112
|
+
const compressedLen = buf.readUInt32LE(state.offset);
|
|
113
|
+
state.offset += 4;
|
|
114
|
+
const decompressedLen = buf.readUInt32LE(state.offset);
|
|
115
|
+
state.offset += 4;
|
|
116
|
+
// Skip unknown (u32LE) + unk2 (u16LE)
|
|
117
|
+
state.offset += 6;
|
|
118
|
+
const compressed = buf.readUInt8(state.offset) !== 0;
|
|
119
|
+
state.offset += 1;
|
|
120
|
+
// Skip compression_level (u8) + timestamp (u32LE)
|
|
121
|
+
state.offset += 5;
|
|
122
|
+
return {
|
|
123
|
+
kind: "file",
|
|
124
|
+
name,
|
|
125
|
+
offset,
|
|
126
|
+
compressedLen,
|
|
127
|
+
decompressedLen,
|
|
128
|
+
compressed,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
133
|
+
function readAt(fd, position, length) {
|
|
134
|
+
const buf = Buffer.alloc(length);
|
|
135
|
+
const bytesRead = readSync(fd, buf, 0, length, position);
|
|
136
|
+
if (bytesRead < length) {
|
|
137
|
+
throw new Error(`Unexpected EOF: wanted ${length} bytes at offset ${position}, got ${bytesRead}`);
|
|
138
|
+
}
|
|
139
|
+
return buf;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../../src/pak/reader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface VfsEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
isDirectory: boolean;
|
|
4
|
+
/** Decompressed size for files, 0 for directories */
|
|
5
|
+
size: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Virtual filesystem that merges all .pak files in the game's addons/ directory
|
|
9
|
+
* into a single unified file tree. Supports directory listing, file existence
|
|
10
|
+
* checks, and on-demand file reading with automatic zlib decompression.
|
|
11
|
+
*
|
|
12
|
+
* Instantiated lazily as a singleton and cached for the session lifetime.
|
|
13
|
+
*/
|
|
14
|
+
export declare class PakVirtualFS {
|
|
15
|
+
private static instance;
|
|
16
|
+
private static instanceGamePath;
|
|
17
|
+
/** Flat lookup: normalized virtual path → file reference */
|
|
18
|
+
private fileIndex;
|
|
19
|
+
/** Merged directory tree for browsing */
|
|
20
|
+
private root;
|
|
21
|
+
/**
|
|
22
|
+
* Get or create the singleton VFS for the given game path.
|
|
23
|
+
* Returns null if no .pak files are found.
|
|
24
|
+
*/
|
|
25
|
+
static get(gamePath: string): PakVirtualFS | null;
|
|
26
|
+
private constructor();
|
|
27
|
+
/**
|
|
28
|
+
* List entries in a virtual directory.
|
|
29
|
+
* Path uses forward slashes, no leading slash (e.g., "Prefabs/Weapons").
|
|
30
|
+
* Empty string = root.
|
|
31
|
+
*/
|
|
32
|
+
listDir(virtualPath: string): VfsEntry[];
|
|
33
|
+
/** Check if a path exists (file or directory). */
|
|
34
|
+
exists(virtualPath: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Read a file's raw bytes from the pak archive.
|
|
37
|
+
* Opens the .pak, seeks to the correct offset, reads, decompresses if needed.
|
|
38
|
+
*/
|
|
39
|
+
readFile(virtualPath: string): Buffer;
|
|
40
|
+
/** Read a file as UTF-8 text. */
|
|
41
|
+
readTextFile(virtualPath: string): string;
|
|
42
|
+
/** Get decompressed file size without reading/inflating. Returns -1 if not found. */
|
|
43
|
+
fileSize(virtualPath: string): number;
|
|
44
|
+
/** Get all file paths in the VFS (for building the asset search index). */
|
|
45
|
+
allFilePaths(): string[];
|
|
46
|
+
/** Get the number of indexed files. */
|
|
47
|
+
get fileCount(): number;
|
|
48
|
+
/**
|
|
49
|
+
* Merge a parsed pak tree into the unified directory tree.
|
|
50
|
+
* Returns the number of file entries added.
|
|
51
|
+
*/
|
|
52
|
+
private mergeTree;
|
|
53
|
+
/** Resolve a virtual path to a directory entry, or null if not found. */
|
|
54
|
+
private resolveDir;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=vfs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vfs.d.ts","sourceRoot":"","sources":["../../src/pak/vfs.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;CACd;AAUD;;;;;;GAMG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA6B;IACpD,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAuB;IAEtD,4DAA4D;IAC5D,OAAO,CAAC,SAAS,CAA8B;IAC/C,yCAAyC;IACzC,OAAO,CAAC,IAAI,CAA+D;IAE3E;;;OAGG;IACH,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IA0BjD,OAAO;IAwBP;;;;OAIG;IACH,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,QAAQ,EAAE;IAexC,kDAAkD;IAClD,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAMpC;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IA8BrC,iCAAiC;IACjC,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAIzC,qFAAqF;IACrF,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAMrC,2EAA2E;IAC3E,YAAY,IAAI,MAAM,EAAE;IAIxB,uCAAuC;IACvC,IAAI,SAAS,IAAI,MAAM,CAEtB;IAID;;;OAGG;IACH,OAAO,CAAC,SAAS;IAqCjB,yEAAyE;IACzE,OAAO,CAAC,UAAU;CAenB"}
|