iets-dev 1.0.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.
Files changed (66) hide show
  1. package/.claude/settings.local.json +59 -0
  2. package/.github/workflows/build.yml +33 -0
  3. package/.github/workflows/npm-publish.yml +24 -0
  4. package/CLAUDE.md +68 -0
  5. package/README.md +21 -0
  6. package/eslint.config.js +34 -0
  7. package/package.json +28 -0
  8. package/pnpm-workspace.yaml +5 -0
  9. package/scripts/barrel-generator.test.ts +155 -0
  10. package/scripts/barrel-generator.ts +207 -0
  11. package/scripts/ensure-iesdp.sh +20 -0
  12. package/scripts/ts-update.sh +15 -0
  13. package/scripts/ts-update.test.ts +136 -0
  14. package/scripts/ts-update.ts +457 -0
  15. package/scripts/utils.ts +32 -0
  16. package/src/CHANGELOG.md +45 -0
  17. package/src/README.md +23 -0
  18. package/src/ambient.d.ts +23 -0
  19. package/src/bg1/index.d.ts +6 -0
  20. package/src/bg2/actions.d.ts +3512 -0
  21. package/src/bg2/align.ids.d.ts +4 -0
  22. package/src/bg2/animate.ids.d.ts +326 -0
  23. package/src/bg2/areaflag.ids.d.ts +4 -0
  24. package/src/bg2/areatype.ids.d.ts +4 -0
  25. package/src/bg2/astyles.ids.d.ts +11 -0
  26. package/src/bg2/class.ids.d.ts +135 -0
  27. package/src/bg2/damages.ids.d.ts +4 -0
  28. package/src/bg2/difflev.ids.d.ts +4 -0
  29. package/src/bg2/dir.ids.ts +23 -0
  30. package/src/bg2/dmgtype.ids.d.ts +4 -0
  31. package/src/bg2/ea.ids.d.ts +5 -0
  32. package/src/bg2/gender.ids.d.ts +4 -0
  33. package/src/bg2/general.ids.d.ts +4 -0
  34. package/src/bg2/gtimes.ids.d.ts +4 -0
  35. package/src/bg2/happy.ids.d.ts +4 -0
  36. package/src/bg2/help.d.ts +42 -0
  37. package/src/bg2/hotkey.ids.d.ts +4 -0
  38. package/src/bg2/index.ts +1809 -0
  39. package/src/bg2/jourtype.ids.d.ts +4 -0
  40. package/src/bg2/kit.ids.d.ts +4 -0
  41. package/src/bg2/mflags.ids.d.ts +4 -0
  42. package/src/bg2/modal.ids.d.ts +14 -0
  43. package/src/bg2/npc.ids.d.ts +4 -0
  44. package/src/bg2/object.d.ts +366 -0
  45. package/src/bg2/object.ts +69 -0
  46. package/src/bg2/race.ids.d.ts +85 -0
  47. package/src/bg2/reaction.ids.d.ts +4 -0
  48. package/src/bg2/scrlev.ids.d.ts +4 -0
  49. package/src/bg2/scroll.ids.d.ts +4 -0
  50. package/src/bg2/seq.ids.d.ts +4 -0
  51. package/src/bg2/shoutids.ids.d.ts +15 -0
  52. package/src/bg2/slots.ids.d.ts +88 -0
  53. package/src/bg2/sndslot.ids.d.ts +4 -0
  54. package/src/bg2/soundoff.ids.d.ts +4 -0
  55. package/src/bg2/specific.ids.d.ts +4 -0
  56. package/src/bg2/spell.ids.d.ts +2008 -0
  57. package/src/bg2/state.ids.d.ts +124 -0
  58. package/src/bg2/stats.ids.d.ts +4 -0
  59. package/src/bg2/time.ids.d.ts +4 -0
  60. package/src/bg2/timeoday.ids.d.ts +4 -0
  61. package/src/bg2/triggers.d.ts +1082 -0
  62. package/src/bg2/weather.ids.d.ts +4 -0
  63. package/src/index.ts +107 -0
  64. package/src/package.json +21 -0
  65. package/src/tsconfig.json +11 -0
  66. package/tsconfig.json +19 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Tests for TypeScript update script utilities.
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { normalizeParamName, parseTriggerParameters, extractTriggerBlocks } from "./ts-update";
7
+
8
+ describe("normalizeParamName", () => {
9
+ describe("lower case strategy (actions)", () => {
10
+ it("lowercases entire name", () => {
11
+ expect(normalizeParamName("TargetName", "lower")).toBe("targetname");
12
+ });
13
+
14
+ it("removes whitespace", () => {
15
+ expect(normalizeParamName("Target Name", "lower")).toBe("targetname");
16
+ });
17
+
18
+ it("resolves reserved name GLOBAL", () => {
19
+ expect(normalizeParamName("GLOBAL", "lower")).toBe("global");
20
+ });
21
+
22
+ it("resolves reserved name class", () => {
23
+ expect(normalizeParamName("class", "lower")).toBe("classID");
24
+ });
25
+
26
+ it("resolves reserved name iD", () => {
27
+ expect(normalizeParamName("iD", "lower")).toBe("id");
28
+ });
29
+ });
30
+
31
+ describe("camelCase strategy (triggers)", () => {
32
+ it("lowercases first char only", () => {
33
+ expect(normalizeParamName("TargetName", "camelCase")).toBe("targetName");
34
+ });
35
+
36
+ it("preserves rest of casing", () => {
37
+ expect(normalizeParamName("AreaType", "camelCase")).toBe("areaType");
38
+ });
39
+
40
+ it("removes whitespace", () => {
41
+ expect(normalizeParamName("Target Name", "camelCase")).toBe("targetName");
42
+ });
43
+
44
+ it("resolves reserved name GLOBAL", () => {
45
+ expect(normalizeParamName("GLOBAL", "camelCase")).toBe("global");
46
+ });
47
+ });
48
+ });
49
+
50
+ describe("parseTriggerParameters", () => {
51
+ it("returns empty string for empty input", () => {
52
+ expect(parseTriggerParameters("")).toBe("");
53
+ });
54
+
55
+ it("parses single parameter with IDS type", () => {
56
+ const result = parseTriggerParameters("I:Style*AStyles");
57
+ expect(result).toBe("style: AStyles");
58
+ });
59
+
60
+ it("parses object parameter", () => {
61
+ const result = parseTriggerParameters("O:Target");
62
+ expect(result).toBe("target: ObjectPtr");
63
+ });
64
+
65
+ it("parses string parameter", () => {
66
+ const result = parseTriggerParameters("S:Name");
67
+ expect(result).toBe("name: string");
68
+ });
69
+
70
+ it("parses multiple parameters", () => {
71
+ const result = parseTriggerParameters("I:Stat*Stats,I:Value,O:Object");
72
+ expect(result).toBe("stat: Stats, value: number, object: ObjectPtr");
73
+ });
74
+
75
+ it("applies type aliases (Spell -> SpellID)", () => {
76
+ const result = parseTriggerParameters("I:Spell*Spell");
77
+ expect(result).toBe("spell: SpellID");
78
+ });
79
+
80
+ it("throws on invalid parameter format", () => {
81
+ expect(() => parseTriggerParameters("invalid")).toThrow("Invalid parameter format");
82
+ });
83
+
84
+ it("throws on unknown type", () => {
85
+ expect(() => parseTriggerParameters("Z:Unknown")).toThrow("Unknown type");
86
+ });
87
+
88
+ it("overrides type for param named Scope via PARAM_NAME_TYPES", () => {
89
+ const result = parseTriggerParameters("S:Name*,S:Scope*");
90
+ expect(result).toBe("name: string, scope: Scope");
91
+ });
92
+
93
+ it("resolves resref type codes (ItmRef, AreRef, SplRef)", () => {
94
+ expect(parseTriggerParameters("ItmRef:Item*")).toBe("item: ItmRef");
95
+ expect(parseTriggerParameters("AreRef:Area*")).toBe("area: AreRef");
96
+ expect(parseTriggerParameters("SplRef:Spell*")).toBe("spell: SplRef");
97
+ });
98
+
99
+ it("does not override non-matching param names", () => {
100
+ const result = parseTriggerParameters("S:MyScope*");
101
+ expect(result).toBe("myScope: string");
102
+ });
103
+ });
104
+
105
+ describe("extractTriggerBlocks", () => {
106
+ it("extracts trigger blocks from HTML content", () => {
107
+ const html = `<html><body>
108
+ Header content
109
+ 0x0001 TriggerOne(I:Value)
110
+ Description of trigger one
111
+ 0x0002 TriggerTwo(O:Object)
112
+ Description of trigger two
113
+ </body></html>`;
114
+ const blocks = extractTriggerBlocks(html);
115
+ expect(blocks.length).toBe(2);
116
+ expect(blocks[0]).toContain("0x0001");
117
+ expect(blocks[1]).toContain("0x0002");
118
+ });
119
+
120
+ it("filters out content before first trigger", () => {
121
+ const html = `<html><body>
122
+ Some preamble text
123
+ 0x0010 OnlyTrigger(S:Name)
124
+ Trigger desc
125
+ </body></html>`;
126
+ const blocks = extractTriggerBlocks(html);
127
+ expect(blocks.length).toBe(1);
128
+ expect(blocks[0]).toContain("0x0010");
129
+ });
130
+
131
+ it("returns empty array for content without triggers", () => {
132
+ const html = "<html><body>No triggers here</body></html>";
133
+ const blocks = extractTriggerBlocks(html);
134
+ expect(blocks.length).toBe(0);
135
+ });
136
+ });
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * TypeScript Update Script
5
+ *
6
+ * Generates TypeScript declarations for BG2 scripting actions and triggers
7
+ * from IESDP YAML and HTML data.
8
+ */
9
+
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ import * as yaml from "js-yaml";
13
+ import yargs from "yargs";
14
+ import { hideBin } from "yargs/helpers";
15
+ import { JSDOM } from "jsdom";
16
+ import { readFile, log } from "./utils.js";
17
+ import { generateBarrelFile } from "./barrel-generator.js";
18
+
19
+ // Constants
20
+ const SKIP_FUNCTION_NAMES = ["Help"]; // Help is both action and trigger
21
+
22
+ /** IESDP param names that are reserved keywords or need normalization in TypeScript. */
23
+ const RESERVED_PARAM_NAMES: Readonly<Record<string, string>> = {
24
+ GLOBAL: "global",
25
+ class: "classID", // `class` is a reserved keyword in TypeScript
26
+ iD: "id",
27
+ };
28
+
29
+ const TYPE_ALIASES: Readonly<Record<string, string>> = {
30
+ Animate: "Animate",
31
+ Class: "CLASS",
32
+ Race: "RACE",
33
+ Spell: "SpellID",
34
+ Weather: "WeatherID",
35
+ ShoutIDS: "ShoutID",
36
+ };
37
+
38
+ const TYPE_MAPPING: Readonly<Record<string, string>> = {
39
+ s: "string",
40
+ o: "ObjectPtr",
41
+ i: "number",
42
+ p: "Point",
43
+ a: "Action",
44
+ areref: "AreRef",
45
+ creref: "CreRef",
46
+ itmref: "ItmRef",
47
+ splref: "SplRef",
48
+ strref: "StrRef",
49
+ };
50
+
51
+ /** Maps YAML param names to TypeScript types. Checked before type/ids resolution. */
52
+ const PARAM_NAME_TYPES: Readonly<Record<string, string>> = {
53
+ Scope: "Scope",
54
+ Face: "Direction",
55
+ Direction: "Direction",
56
+ };
57
+
58
+ const ACTION_FILE_HEADER = `import type { Action, AreRef, CreRef, ItmRef, ObjectPtr, Point, Scope, SpellID, SplRef, StrRef } from "../index";
59
+
60
+ import type { Direction } from "./dir.ids";
61
+ import type { Align } from "./align.ids";
62
+ import type { Animate } from "./animate.ids";
63
+ import type { AreaFlag } from "./areaflag.ids";
64
+ import type { AreaTypeID as AreaType } from "./areatype.ids";
65
+ import type { CLASS } from "./class.ids";
66
+ import type { DMGtype } from "./dmgtype.ids";
67
+ import type { EA } from "./ea.ids";
68
+ import type { GenderID as Gender } from "./gender.ids";
69
+ import type { GeneralID as General } from "./general.ids";
70
+ import type { GTimes } from "./gtimes.ids";
71
+ import type { MFlags } from "./mflags.ids";
72
+ import type { JourType } from "./jourtype.ids";
73
+ import type { KitID as Kit } from "./kit.ids";
74
+ import type { RACE } from "./race.ids";
75
+ import type { ScrLev } from "./scrlev.ids";
76
+ import type { Scroll } from "./scroll.ids";
77
+ import type { Seq } from "./seq.ids";
78
+ import type { ShoutID } from "./shoutids.ids";
79
+ import type { Slots } from "./slots.ids";
80
+ import type { SndSlot } from "./sndslot.ids";
81
+ import type { SoundOff } from "./soundoff.ids";
82
+ import type { Specific } from "./specific.ids";
83
+ import type { TimeID as Time } from "./time.ids";
84
+ import type { WeatherID } from "./weather.ids";
85
+
86
+ `;
87
+
88
+ const TRIGGER_FILE_HEADER = `import type { AreRef, ItmRef, ObjectPtr, Scope, SplRef, SpellID } from "../index";
89
+
90
+ import type { Align } from "./align.ids";
91
+ import type { AreaTypeID as AreaType } from "./areatype.ids";
92
+ import type { AStyles } from "./astyles.ids";
93
+ import type { CLASS } from "./class.ids";
94
+ import type { Damages } from "./damages.ids";
95
+ import type { DiffLev } from "./difflev.ids";
96
+ import type { EA } from "./ea.ids";
97
+ import type { GenderID as Gender } from "./gender.ids";
98
+ import type { GeneralID as General } from "./general.ids";
99
+ import type { Happy } from "./happy.ids";
100
+ import type { HotKeyID as HotKey } from "./hotkey.ids";
101
+ import type { KitID as Kit } from "./kit.ids";
102
+ import type { NPC } from "./npc.ids";
103
+ import type { Modal } from "./modal.ids";
104
+ import type { RACE } from "./race.ids";
105
+ import type { ReactionID as Reaction } from "./reaction.ids";
106
+ import type { ShoutID } from "./shoutids.ids";
107
+ import type { Slots } from "./slots.ids";
108
+ import type { Specific } from "./specific.ids";
109
+ import type { State } from "./state.ids";
110
+ import type { Stats } from "./stats.ids";
111
+ import type { TimeID as Time } from "./time.ids";
112
+ import type { TimeODay } from "./timeoday.ids";
113
+
114
+ `;
115
+
116
+ // Types
117
+ interface Parameter {
118
+ name: string;
119
+ type: string;
120
+ ids?: string;
121
+ }
122
+
123
+ interface ParsedActionData {
124
+ bg2: number;
125
+ unknown?: boolean;
126
+ no_result?: boolean;
127
+ name: string;
128
+ desc?: string;
129
+ params?: Parameter[];
130
+ }
131
+
132
+ /**
133
+ * Validates that parsed YAML matches the expected ParsedActionData shape.
134
+ */
135
+ function isValidActionData(data: unknown): data is ParsedActionData {
136
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
137
+ return false;
138
+ }
139
+ return "bg2" in data && typeof data.bg2 === "number"
140
+ && "name" in data && typeof data.name === "string";
141
+ }
142
+
143
+ /**
144
+ * Normalizes a type name to its canonical form.
145
+ */
146
+ function normalizeTypeName(typeName: string): string {
147
+ return TYPE_ALIASES[typeName] ?? typeName;
148
+ }
149
+
150
+ /**
151
+ * Normalizes a parameter name with a given case strategy.
152
+ * - 'lower': lowercases entire name (used for action params)
153
+ * - 'camelCase': lowercases first char only, keeps rest (used for trigger params)
154
+ */
155
+ export function normalizeParamName(name: string, caseStrategy: "lower" | "camelCase"): string {
156
+ // Check for known reserved/special names first
157
+ const reserved = RESERVED_PARAM_NAMES[name];
158
+ if (reserved) {
159
+ return reserved;
160
+ }
161
+
162
+ // Remove whitespace and apply case strategy
163
+ const sanitized = name.replace(/\s+/g, "");
164
+ const normalized = caseStrategy === "lower"
165
+ ? sanitized.toLowerCase()
166
+ : sanitized.charAt(0).toLowerCase() + sanitized.slice(1);
167
+
168
+ // Check again after normalization for reserved names
169
+ return RESERVED_PARAM_NAMES[normalized] ?? normalized;
170
+ }
171
+
172
+ /**
173
+ * Validates that a value is a non-null object.
174
+ */
175
+ function isValidObject(value: unknown): value is Record<string, unknown> {
176
+ return value !== null && typeof value === "object" && !Array.isArray(value);
177
+ }
178
+
179
+ /**
180
+ * Prettifies code blocks in descriptions.
181
+ */
182
+ function processCodeBlocks(description: string): string {
183
+ return description.replace(/```(.*?)```/gs, (_match, code: string) => {
184
+ const indentedCode = code
185
+ .trim()
186
+ .split("\n")
187
+ .map((line) => ` ${line}`)
188
+ .join("\n");
189
+ return `\`\`\`weidu-baf\n${indentedCode}\n\`\`\``;
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Reads and parses a YAML action file, converting it to a TS declaration.
195
+ */
196
+ function generateTypeScriptDeclaration(yamlFilePath: string): string | null {
197
+ const fileContent = readFile(yamlFilePath);
198
+ const parsed = yaml.load(fileContent);
199
+
200
+ if (!isValidObject(parsed)) {
201
+ log(`Warning: ${yamlFilePath}: invalid YAML structure (expected object, got ${typeof parsed})`);
202
+ return null;
203
+ }
204
+
205
+ if (!isValidActionData(parsed)) {
206
+ const obj = parsed;
207
+ // Files for other game variants (bg1, bgee, pst, iwd) lack bg2 field -- expected, skip silently
208
+ if (!("bg2" in obj)) {
209
+ return null;
210
+ }
211
+ const missing = [
212
+ typeof obj.bg2 !== "number" ? "bg2 (number)" : "",
213
+ typeof obj.name !== "string" ? "name (string)" : "",
214
+ ].filter(Boolean);
215
+ log(`Warning: ${yamlFilePath}: missing required fields: ${missing.join(", ")}`);
216
+ return null;
217
+ }
218
+
219
+ // bg2 field exists but is not 1 (e.g. 0 = not applicable to BG2)
220
+ if (parsed.bg2 !== 1) {
221
+ return null;
222
+ }
223
+
224
+ // unknown/no_result actions have no usable signature
225
+ if (parsed.unknown || parsed.no_result) {
226
+ return null;
227
+ }
228
+
229
+ const functionName = parsed.name;
230
+ if (SKIP_FUNCTION_NAMES.includes(functionName)) {
231
+ log(`Skipping ${yamlFilePath}: ${functionName}() is in skip list`);
232
+ return null;
233
+ }
234
+
235
+ let description = processCodeBlocks(parsed.desc ?? "");
236
+ // Replace block-style comments with single-line comments
237
+ description = description.replace(/\/\* (.*?) \*\//g, "// $1");
238
+
239
+ const params: Parameter[] = parsed.params ?? [];
240
+ const paramLines: string[] = [];
241
+ let unusedCount = 0;
242
+
243
+ for (const param of params) {
244
+ let paramName = param.name.toLowerCase() === "unused" ? `unused${unusedCount++}` : param.name;
245
+ paramName = normalizeParamName(paramName, "lower");
246
+
247
+ // Priority: param name exact override > ids field > type code
248
+ const typeOverride = PARAM_NAME_TYPES[param.name];
249
+ // Use explicit empty-string check: ids may be "" which should fall through to TYPE_MAPPING
250
+ const paramType = typeOverride ?? normalizeTypeName(
251
+ param.ids !== undefined && param.ids !== "" ? param.ids : TYPE_MAPPING[param.type] ?? param.type,
252
+ );
253
+ paramLines.push(`${paramName}: ${paramType}`);
254
+ }
255
+
256
+ const paramsStr = paramLines.join(", ");
257
+ return `/**\n * ${description.trim()}\n */\nexport declare function ${functionName}(${paramsStr}): Action;`;
258
+ }
259
+
260
+ /**
261
+ * Processes a directory of YAML action files and generates TypeScript declarations.
262
+ */
263
+ function processActionFiles(directory: string, outputFile: string): void {
264
+ if (!fs.existsSync(directory)) {
265
+ throw new Error(`Actions directory not found: ${directory}`);
266
+ }
267
+
268
+ const tsOutput: string[] = [ACTION_FILE_HEADER];
269
+ let processedCount = 0;
270
+ let skippedCount = 0;
271
+
272
+ const files = fs.readdirSync(directory);
273
+ for (const file of files) {
274
+ if (!file.toLowerCase().endsWith(".yml")) {
275
+ continue;
276
+ }
277
+
278
+ const yamlFilePath = path.join(directory, file);
279
+ const declaration = generateTypeScriptDeclaration(yamlFilePath);
280
+
281
+ if (declaration) {
282
+ tsOutput.push(declaration);
283
+ processedCount++;
284
+ } else {
285
+ skippedCount++;
286
+ }
287
+ }
288
+
289
+ fs.writeFileSync(outputFile, tsOutput.join("\n\n"), "utf-8");
290
+ log(`Actions: ${processedCount} processed, ${skippedCount} skipped. Output: ${outputFile}`);
291
+ }
292
+
293
+ /**
294
+ * Parses HTML trigger file and extracts trigger blocks.
295
+ */
296
+ export function extractTriggerBlocks(fileContent: string): string[] {
297
+ const dom = new JSDOM(fileContent);
298
+ const textContent = dom.window.document.body.textContent;
299
+
300
+ // Split text content into blocks based on lines starting with '0x'
301
+ // Filter out any content before the first trigger (frontmatter, headers, etc.)
302
+ return textContent
303
+ .split(/\s+(?=0x[0-9A-Fa-f]{4})/)
304
+ .map((block) => block.trim())
305
+ .filter((block) => block.startsWith("0x"));
306
+ }
307
+
308
+ /**
309
+ * Converts a trigger block to a TypeScript declaration.
310
+ * Block format: "0xNNNN TriggerName(params)\nDescription..."
311
+ */
312
+ function convertTriggerBlockToDeclaration(block: string): string {
313
+ const lines = block.split("\n").map((line) => line.trim());
314
+ const header = lines[0];
315
+ if (!header) {
316
+ throw new Error(`Empty trigger block`);
317
+ }
318
+ const description = lines.slice(1).join(" ");
319
+
320
+ // Expected format: 0xNNNN FunctionName(params)
321
+ const headerMatch = header.match(/^0x[0-9A-Fa-f]+ (\w+)\((.*?)\)$/);
322
+ if (!headerMatch) {
323
+ throw new Error(`Invalid trigger header format: "${header}"`);
324
+ }
325
+
326
+ const triggerName = headerMatch[1];
327
+ const params = headerMatch[2];
328
+ if (!triggerName || params === undefined) {
329
+ throw new Error(`Failed to parse trigger name/params from: "${header}"`);
330
+ }
331
+ const paramsStr = parseTriggerParameters(params);
332
+
333
+ return `/**\n * ${description.trim()}\n */\nexport declare function ${triggerName}(${paramsStr}): boolean;`;
334
+ }
335
+
336
+ /**
337
+ * Parses trigger parameter string to TypeScript format.
338
+ * Input format: "I:Name*IDS,O:Target,S:String"
339
+ */
340
+ export function parseTriggerParameters(params: string): string {
341
+ if (params === "") {
342
+ return params;
343
+ }
344
+
345
+ return params
346
+ .split(",")
347
+ .map((param) => {
348
+ const [type, nameWithDetails] = param.split(":").map((part) => part.trim());
349
+ if (!type || !nameWithDetails) {
350
+ throw new Error(`Invalid parameter format: "${param}"`);
351
+ }
352
+
353
+ // Extract name and specific type (e.g., "Style*AStyles" -> name="Style", specificType="AStyles")
354
+ const parts = nameWithDetails.split("*").map((part) => part.trim());
355
+ const name = parts[0];
356
+ if (!name) {
357
+ throw new Error(`Empty parameter name in: "${param}"`);
358
+ }
359
+ const specificType = parts.length > 1 ? parts[1] : undefined;
360
+ const formattedName = normalizeParamName(name, "camelCase");
361
+
362
+ // Priority: param name exact override > specific IDS type > base type code
363
+ const typeOverride = PARAM_NAME_TYPES[name];
364
+ const rawType = typeOverride ?? (specificType !== undefined && specificType !== ""
365
+ ? specificType
366
+ : TYPE_MAPPING[type.toLowerCase()]);
367
+ if (!rawType) {
368
+ throw new Error(`Unknown type: "${type}" or "${specificType}"`);
369
+ }
370
+ const tsType = normalizeTypeName(rawType);
371
+
372
+ return `${formattedName}: ${tsType}`;
373
+ })
374
+ .join(", ");
375
+ }
376
+
377
+ /**
378
+ * Processes trigger HTML file and generates TypeScript declarations.
379
+ */
380
+ function processTriggers(triggerFilePath: string, triggerOutputFilePath: string): void {
381
+ if (!fs.existsSync(triggerFilePath)) {
382
+ throw new Error(`Triggers file not found: ${triggerFilePath}`);
383
+ }
384
+
385
+ const fileContent = readFile(triggerFilePath);
386
+ const triggerBlocks = extractTriggerBlocks(fileContent);
387
+
388
+ log(`Found ${triggerBlocks.length} trigger blocks`);
389
+
390
+ const tsOutput: string[] = [TRIGGER_FILE_HEADER];
391
+ let errorCount = 0;
392
+
393
+ for (const [i, block] of triggerBlocks.entries()) {
394
+ // Skip triggers that are handled separately (e.g. Help is both action and trigger)
395
+ const nameMatch = block.match(/^0x[0-9A-Fa-f]+ (\w+)\(/);
396
+ if (nameMatch?.[1] && SKIP_FUNCTION_NAMES.includes(nameMatch[1])) {
397
+ log(`Skipping trigger block #${i + 1}: ${nameMatch[1]}() is in skip list`);
398
+ continue;
399
+ }
400
+
401
+ try {
402
+ const declaration = convertTriggerBlockToDeclaration(block);
403
+ tsOutput.push(declaration);
404
+ } catch (error) {
405
+ const message = error instanceof Error ? error.message : String(error);
406
+ log(`Warning: Failed to process block #${i + 1}: ${message}`);
407
+ log(block);
408
+ errorCount++;
409
+ }
410
+ }
411
+
412
+ if (tsOutput.length === 1) {
413
+ throw new Error("No valid trigger blocks were processed");
414
+ }
415
+
416
+ fs.writeFileSync(triggerOutputFilePath, tsOutput.join("\n\n"), "utf-8");
417
+ log(`Trigger declarations written to ${triggerOutputFilePath} (${errorCount} errors)`);
418
+ }
419
+
420
+ // Main entry point
421
+ function main(): void {
422
+ const argv = yargs(hideBin(process.argv))
423
+ .scriptName("ts-update")
424
+ .usage("Usage: $0 <actions_directory> <actions_output_file> <triggers_file> <triggers_output_file>")
425
+ .demandCommand(4)
426
+ .help()
427
+ .parseSync();
428
+
429
+ const args = argv._.map(String);
430
+ const actionsDirectory = args[0];
431
+ const actionsOutputFile = args[1];
432
+ const triggersFile = args[2];
433
+ const triggersOutputFile = args[3];
434
+
435
+ if (!actionsDirectory || !actionsOutputFile || !triggersFile || !triggersOutputFile) {
436
+ throw new Error("Expected 4 positional arguments");
437
+ }
438
+
439
+ processActionFiles(actionsDirectory, actionsOutputFile);
440
+ processTriggers(triggersFile, triggersOutputFile);
441
+
442
+ // Generate barrel file from the same directory as the actions output
443
+ const bg2Dir = path.dirname(actionsOutputFile);
444
+ generateBarrelFile(bg2Dir);
445
+ }
446
+
447
+ // Only run CLI when executed directly (not imported as a module for testing)
448
+ const isDirectExecution = process.argv[1]?.endsWith("ts-update.ts") ?? false;
449
+ if (isDirectExecution) {
450
+ try {
451
+ main();
452
+ } catch (error) {
453
+ const message = error instanceof Error ? error.message : String(error);
454
+ console.error(`Error: ${message}`);
455
+ process.exit(1);
456
+ }
457
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared utilities for build scripts.
3
+ */
4
+
5
+ import * as fs from "fs";
6
+
7
+ /**
8
+ * Reads a file and returns its content, throwing descriptive errors.
9
+ */
10
+ export function readFile(filePath: string): string {
11
+ if (!fs.existsSync(filePath)) {
12
+ throw new Error(`File not found: ${filePath}`);
13
+ }
14
+ return fs.readFileSync(filePath, "utf-8");
15
+ }
16
+
17
+ /**
18
+ * Validates that a directory exists.
19
+ */
20
+ export function validateDirectory(dirPath: string, description: string): void {
21
+ if (!fs.existsSync(dirPath)) {
22
+ throw new Error(`${description} not found: ${dirPath}`);
23
+ }
24
+ if (!fs.statSync(dirPath).isDirectory()) {
25
+ throw new Error(`${description} is not a directory: ${dirPath}`);
26
+ }
27
+ }
28
+
29
+ /** Logs a message to stdout. */
30
+ export function log(message: string): void {
31
+ process.stdout.write(`${message}\n`);
32
+ }
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ ## 0.3.1
4
+
5
+ - Package renamed from `ielib` to `@bgforge/iets`.
6
+ - Repository split from [BGforge-MLS-IElib](https://github.com/BGforgeNet/BGforge-MLS-IElib) into standalone [iets](https://github.com/BGforgeNet/iets).
7
+
8
+ ## 0.3.0
9
+
10
+ - `Animate`, `CLASS`, `RACE` are now `declare enum` -- use `Animate.ANKHEG`, `CLASS.FIGHTER`, `RACE.HUMAN`.
11
+ - Add `RACE` enum with full race.ids data.
12
+ - Add `Direction` enum, modal.ids constants, `ItmRef` type, `tlk()` function.
13
+ - Object specifiers (`LastAttackerOf`, etc.) accept an optional target parameter.
14
+ - `tra()` returns `StrRef`.
15
+
16
+ ### Breaking changes
17
+
18
+ - Bare IDS constant exports (`ANKHEG`, `FIGHTER`, etc.) from animate.ids and class.ids removed. Use enum members instead.
19
+ - `ClassID` type renamed to `CLASS`; `RaceID` type renamed to `RACE`.
20
+
21
+ ## 0.2.0
22
+
23
+ - Add `CreRef`, `StrRef`, `Direction` types.
24
+ - Action/trigger params use typed refs (`AreRef`, `CreRef`, `StrRef`) and `Direction` where applicable.
25
+
26
+ ## 0.1.4
27
+
28
+ Add `Action` type for actions.
29
+
30
+ ## 0.1.3
31
+
32
+ Switch `$tra` to `tra` and `$obj` to `obj`.
33
+
34
+ ## 0.1.2
35
+
36
+ - Add bg2 animate.ids.
37
+ - Add death vars to `$obj` description.
38
+
39
+ ## 0.1.1
40
+
41
+ Same as initial release.
42
+
43
+ ## 0.1
44
+
45
+ Initial release.
package/src/README.md ADDED
@@ -0,0 +1,23 @@
1
+ ## IElib
2
+
3
+ Typescript bindings for Infinity Engine BAF.
4
+
5
+ ### Type branding
6
+
7
+ Numeric IDS types (e.g. `SpellID`, `Align`, `ClassID`, `Slots`) use branded types via `IE<number, "...">` for nominal type safety. This prevents accidentally passing one kind of ID where another is expected. Custom numeric values can be created with a cast:
8
+
9
+ ```typescript
10
+ const mySpell = 42 as SpellID;
11
+ ```
12
+
13
+ String resource reference types (`ResRef`, `SplRef`, `ItmRef`) are intentionally **not** branded. Resrefs are almost always raw string literals, and branding would require a cast on every usage (e.g. `"SWORD01" as ItmRef`). Unlike numeric IDS types, there is no finite set of valid resrefs to provide as pre-typed constants.
14
+
15
+ Engine action functions return a branded `Action` type, which allows `ActionOverride` to enforce that its argument is an actual action call rather than an arbitrary expression.
16
+
17
+ ### Named re-exports
18
+
19
+ Barrel `index.ts` files use explicit named re-exports (`export { X } from './module'` / `export type { X } from './module'`) instead of `export *`. esbuild cannot statically enumerate exports from externalized `.d.ts` modules behind `export *` and falls back to runtime `__reExport` helpers, which break downstream transpilers. Named re-exports let esbuild resolve each binding at build time.
20
+
21
+ ### IDS type naming
22
+
23
+ Some IDS types use a `*ID` suffix (`ClassID`, `GenderID`, `KitID`, etc.) while others use bare names (`Align`, `EA`, `State`, etc.). The suffix exists to avoid name clashes with same-named trigger/action functions (e.g. `Class()` trigger vs `ClassID` type). Types without a same-named function use the bare name.