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.
- package/.claude/settings.local.json +59 -0
- package/.github/workflows/build.yml +33 -0
- package/.github/workflows/npm-publish.yml +24 -0
- package/CLAUDE.md +68 -0
- package/README.md +21 -0
- package/eslint.config.js +34 -0
- package/package.json +28 -0
- package/pnpm-workspace.yaml +5 -0
- package/scripts/barrel-generator.test.ts +155 -0
- package/scripts/barrel-generator.ts +207 -0
- package/scripts/ensure-iesdp.sh +20 -0
- package/scripts/ts-update.sh +15 -0
- package/scripts/ts-update.test.ts +136 -0
- package/scripts/ts-update.ts +457 -0
- package/scripts/utils.ts +32 -0
- package/src/CHANGELOG.md +45 -0
- package/src/README.md +23 -0
- package/src/ambient.d.ts +23 -0
- package/src/bg1/index.d.ts +6 -0
- package/src/bg2/actions.d.ts +3512 -0
- package/src/bg2/align.ids.d.ts +4 -0
- package/src/bg2/animate.ids.d.ts +326 -0
- package/src/bg2/areaflag.ids.d.ts +4 -0
- package/src/bg2/areatype.ids.d.ts +4 -0
- package/src/bg2/astyles.ids.d.ts +11 -0
- package/src/bg2/class.ids.d.ts +135 -0
- package/src/bg2/damages.ids.d.ts +4 -0
- package/src/bg2/difflev.ids.d.ts +4 -0
- package/src/bg2/dir.ids.ts +23 -0
- package/src/bg2/dmgtype.ids.d.ts +4 -0
- package/src/bg2/ea.ids.d.ts +5 -0
- package/src/bg2/gender.ids.d.ts +4 -0
- package/src/bg2/general.ids.d.ts +4 -0
- package/src/bg2/gtimes.ids.d.ts +4 -0
- package/src/bg2/happy.ids.d.ts +4 -0
- package/src/bg2/help.d.ts +42 -0
- package/src/bg2/hotkey.ids.d.ts +4 -0
- package/src/bg2/index.ts +1809 -0
- package/src/bg2/jourtype.ids.d.ts +4 -0
- package/src/bg2/kit.ids.d.ts +4 -0
- package/src/bg2/mflags.ids.d.ts +4 -0
- package/src/bg2/modal.ids.d.ts +14 -0
- package/src/bg2/npc.ids.d.ts +4 -0
- package/src/bg2/object.d.ts +366 -0
- package/src/bg2/object.ts +69 -0
- package/src/bg2/race.ids.d.ts +85 -0
- package/src/bg2/reaction.ids.d.ts +4 -0
- package/src/bg2/scrlev.ids.d.ts +4 -0
- package/src/bg2/scroll.ids.d.ts +4 -0
- package/src/bg2/seq.ids.d.ts +4 -0
- package/src/bg2/shoutids.ids.d.ts +15 -0
- package/src/bg2/slots.ids.d.ts +88 -0
- package/src/bg2/sndslot.ids.d.ts +4 -0
- package/src/bg2/soundoff.ids.d.ts +4 -0
- package/src/bg2/specific.ids.d.ts +4 -0
- package/src/bg2/spell.ids.d.ts +2008 -0
- package/src/bg2/state.ids.d.ts +124 -0
- package/src/bg2/stats.ids.d.ts +4 -0
- package/src/bg2/time.ids.d.ts +4 -0
- package/src/bg2/timeoday.ids.d.ts +4 -0
- package/src/bg2/triggers.d.ts +1082 -0
- package/src/bg2/weather.ids.d.ts +4 -0
- package/src/index.ts +107 -0
- package/src/package.json +21 -0
- package/src/tsconfig.json +11 -0
- 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
|
+
}
|
package/scripts/utils.ts
ADDED
|
@@ -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
|
+
}
|
package/src/CHANGELOG.md
ADDED
|
@@ -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.
|