@px-lsp/protocol 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.
Files changed (48) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +25 -0
  3. package/dist/arrays.d.ts +13 -0
  4. package/dist/arrays.js +19 -0
  5. package/dist/constants.d.ts +9 -0
  6. package/dist/constants.js +10 -0
  7. package/dist/descriptorMetadata.d.ts +51 -0
  8. package/dist/descriptorMetadata.js +98 -0
  9. package/dist/descriptorMod.d.ts +66 -0
  10. package/dist/descriptorMod.js +335 -0
  11. package/dist/errorLogParser.d.ts +33 -0
  12. package/dist/errorLogParser.js +125 -0
  13. package/dist/fsWalk.d.ts +20 -0
  14. package/dist/fsWalk.js +159 -0
  15. package/dist/locProperties.d.ts +13 -0
  16. package/dist/locProperties.js +46 -0
  17. package/dist/locRefs.d.ts +11 -0
  18. package/dist/locRefs.js +31 -0
  19. package/dist/modName.d.ts +6 -0
  20. package/dist/modName.js +53 -0
  21. package/dist/protocol.d.ts +1462 -0
  22. package/dist/protocol.js +201 -0
  23. package/dist/regex.d.ts +13 -0
  24. package/dist/regex.js +21 -0
  25. package/dist/suppression.d.ts +52 -0
  26. package/dist/suppression.js +173 -0
  27. package/dist/tigerParser.d.ts +28 -0
  28. package/dist/tigerParser.js +72 -0
  29. package/dist/translationCore.d.ts +26 -0
  30. package/dist/translationCore.js +162 -0
  31. package/dist/types.d.ts +82 -0
  32. package/dist/types.js +3 -0
  33. package/package.json +39 -0
  34. package/src/arrays.ts +16 -0
  35. package/src/constants.ts +12 -0
  36. package/src/descriptorMetadata.ts +101 -0
  37. package/src/descriptorMod.ts +354 -0
  38. package/src/errorLogParser.ts +136 -0
  39. package/src/fsWalk.ts +126 -0
  40. package/src/locProperties.ts +43 -0
  41. package/src/locRefs.ts +38 -0
  42. package/src/modName.ts +18 -0
  43. package/src/protocol.ts +1459 -0
  44. package/src/regex.ts +19 -0
  45. package/src/suppression.ts +178 -0
  46. package/src/tigerParser.ts +79 -0
  47. package/src/translationCore.ts +140 -0
  48. package/src/types.ts +90 -0
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @px-lsp/protocol
2
+
3
+ The wire contract of the [px-lsp language server](https://github.com/JDeffner/paradox-modding-toolkit):
4
+ custom LSP request/notification names, their payload types, the settings and
5
+ initialization-option shapes, plus a few pure helpers shared between the
6
+ server and its clients (tiger report parsing, `.mod` descriptor parsing,
7
+ diagnostic suppression, localization helpers).
8
+
9
+ The published package ships compiled JavaScript with type declarations, so it
10
+ works from plain Node and from any bundler:
11
+
12
+ ```ts
13
+ // The root export is the wire contract (request/notification names + payload types):
14
+ import { modOverviewRequest, type ModOverview } from "@px-lsp/protocol";
15
+ // The helpers live in named modules:
16
+ import { parseTigerJson } from "@px-lsp/protocol/tigerParser";
17
+ import { parseDescriptor } from "@px-lsp/protocol/descriptorMod";
18
+ ```
19
+
20
+ Clients in other languages should code against the documented contract instead:
21
+ see [`docs/PROTOCOL.md`](https://github.com/JDeffner/paradox-modding-toolkit/blob/main/docs/PROTOCOL.md)
22
+ in the repository. Changes to the wire contract are treated as API changes
23
+ and versioned with the packages.
24
+
25
+ License: GPL-3.0-or-later.
@@ -0,0 +1,13 @@
1
+ /** Array helpers shared by the server and the client. */
2
+ /**
3
+ * Append every element of `source` to `target`.
4
+ *
5
+ * `target.push(...source)` passes one argument per element and throws
6
+ * `RangeError: Maximum call stack size exceeded` past ~125k elements (measured,
7
+ * node 24, default stack). On the index paths that is a size-triggered crash:
8
+ * one engine/vanilla root already carries ~460k definitions and one generated
9
+ * mod file can carry six figures on its own. The loop has no ceiling and
10
+ * measures the same as the spread (2M elements appended in 10k pieces: 23 ms
11
+ * loop vs 22 ms spread; as one 500k piece: 5.5 ms loop vs 9.6 ms spread).
12
+ */
13
+ export declare function pushAll<T>(target: T[], source: readonly T[]): void;
package/dist/arrays.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ /** Array helpers shared by the server and the client. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.pushAll = pushAll;
5
+ /**
6
+ * Append every element of `source` to `target`.
7
+ *
8
+ * `target.push(...source)` passes one argument per element and throws
9
+ * `RangeError: Maximum call stack size exceeded` past ~125k elements (measured,
10
+ * node 24, default stack). On the index paths that is a size-triggered crash:
11
+ * one engine/vanilla root already carries ~460k definitions and one generated
12
+ * mod file can carry six figures on its own. The loop has no ceiling and
13
+ * measures the same as the spread (2M elements appended in 10k pieces: 23 ms
14
+ * loop vs 22 ms spread; as one 500k piece: 5.5 ms loop vs 9.6 ms spread).
15
+ */
16
+ function pushAll(target, source) {
17
+ for (let i = 0; i < source.length; i++)
18
+ target.push(source[i]);
19
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Constants needed on both sides of the LSP boundary.
3
+ */
4
+ import type { TokenKind } from "./types";
5
+ /** The script_docs log files and the token kind each one contributes. */
6
+ export declare const LOG_FILES: Array<{
7
+ file: string;
8
+ kind: TokenKind;
9
+ }>;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LOG_FILES = void 0;
4
+ /** The script_docs log files and the token kind each one contributes. */
5
+ exports.LOG_FILES = [
6
+ { file: "triggers.log", kind: "trigger" },
7
+ { file: "effects.log", kind: "effect" },
8
+ { file: "event_targets.log", kind: "event_target" },
9
+ { file: "modifiers.log", kind: "modifier" },
10
+ ];
@@ -0,0 +1,51 @@
1
+ /** Mod-root-relative path of the descriptor, forward slashes. */
2
+ export declare const METADATA_REL_PATH = ".metadata/metadata.json";
3
+ /** One entry of `relationships`: a link to another mod. */
4
+ export interface MetadataRelationship {
5
+ /** "dependency", "incompatible_with", "load_before", "load_after". */
6
+ rel_type: string;
7
+ /** The other mod's `id` field (NOT its Workshop number). */
8
+ id: string;
9
+ /** Shown when the other mod is not on disk. */
10
+ display_name?: string;
11
+ /** Only "mod" is supported by the launcher today. */
12
+ resource_type: string;
13
+ /** Version of the other mod, `*` for any. */
14
+ version?: string;
15
+ }
16
+ /** The fields of a mod's metadata.json this toolkit reads or writes. */
17
+ export interface ModMetadata {
18
+ name?: string;
19
+ id?: string;
20
+ version?: string;
21
+ supported_game_version?: string;
22
+ short_description?: string;
23
+ tags?: string[];
24
+ relationships?: MetadataRelationship[];
25
+ game_custom_data?: {
26
+ multiplayer_synchronized?: boolean;
27
+ replace_paths?: string[];
28
+ };
29
+ }
30
+ /** The parsed `<dir>/.metadata/metadata.json`, or null when absent/unreadable. */
31
+ export declare function readMetadata(dir: string): ModMetadata | null;
32
+ /** The mod's display name from `<dir>/.metadata/metadata.json`, or null. */
33
+ export declare function readMetadataName(dir: string): string | null;
34
+ /** True when `dir` carries a metadata-style descriptor. */
35
+ export declare function hasMetadataDescriptor(dir: string): boolean;
36
+ export interface MetadataScaffold {
37
+ name: string;
38
+ /** Stable identifier other mods point their relationships at. */
39
+ id: string;
40
+ /** The mod's own version, not the game's. */
41
+ version?: string;
42
+ /** Game version the mod is for, `*` when unknown. */
43
+ supportedGameVersion: string;
44
+ shortDescription?: string;
45
+ tags?: string[];
46
+ relationships?: MetadataRelationship[];
47
+ /** Vanilla folders the mod unloads wholesale (total conversions). */
48
+ replacePaths?: string[];
49
+ }
50
+ /** A launcher-correct starter metadata.json, in the corpus's field order. */
51
+ export declare function scaffoldMetadata(opts: MetadataScaffold): string;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.METADATA_REL_PATH = void 0;
37
+ exports.readMetadata = readMetadata;
38
+ exports.readMetadataName = readMetadataName;
39
+ exports.hasMetadataDescriptor = hasMetadataDescriptor;
40
+ exports.scaffoldMetadata = scaffoldMetadata;
41
+ /**
42
+ * Reader and writer for the newer Paradox mod descriptor convention:
43
+ * `<mod>/.metadata/metadata.json` (newer titles) instead of the launcher
44
+ * `.mod` file. Fail-soft on read: any read/parse problem yields null.
45
+ *
46
+ * The field set is copied from three real workshop mods (2026-08-12: name, id,
47
+ * version, supported_game_version, tags, relationships, game_custom_data;
48
+ * `game_id` appears in one of the three and is left out here because the other
49
+ * two load without it). The relationship shape is the one the Community Mod
50
+ * Framework documents for the mods that depend on it.
51
+ */
52
+ const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
54
+ /** Mod-root-relative path of the descriptor, forward slashes. */
55
+ exports.METADATA_REL_PATH = ".metadata/metadata.json";
56
+ /** The parsed `<dir>/.metadata/metadata.json`, or null when absent/unreadable. */
57
+ function readMetadata(dir) {
58
+ try {
59
+ const file = path.join(dir, ".metadata", "metadata.json");
60
+ if (!fs.existsSync(file))
61
+ return null;
62
+ return JSON.parse(fs.readFileSync(file, "utf8"));
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /** The mod's display name from `<dir>/.metadata/metadata.json`, or null. */
69
+ function readMetadataName(dir) {
70
+ const name = readMetadata(dir)?.name;
71
+ return typeof name === "string" && name.trim() !== "" ? name : null;
72
+ }
73
+ /** True when `dir` carries a metadata-style descriptor. */
74
+ function hasMetadataDescriptor(dir) {
75
+ try {
76
+ return fs.existsSync(path.join(dir, ".metadata", "metadata.json"));
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ /** A launcher-correct starter metadata.json, in the corpus's field order. */
83
+ function scaffoldMetadata(opts) {
84
+ const body = {
85
+ name: opts.name,
86
+ id: opts.id,
87
+ version: opts.version ?? "0.1.0",
88
+ supported_game_version: opts.supportedGameVersion,
89
+ ...(opts.shortDescription ? { short_description: opts.shortDescription } : {}),
90
+ tags: opts.tags ?? [],
91
+ relationships: opts.relationships ?? [],
92
+ game_custom_data: {
93
+ multiplayer_synchronized: true,
94
+ ...(opts.replacePaths && opts.replacePaths.length > 0 ? { replace_paths: opts.replacePaths } : {}),
95
+ },
96
+ };
97
+ return JSON.stringify(body, null, 2) + "\n";
98
+ }
@@ -0,0 +1,66 @@
1
+ export interface DescriptorField {
2
+ key: string;
3
+ /** Launcher refuses/misbehaves without it. */
4
+ required: boolean;
5
+ /** May appear multiple times (replace_path). */
6
+ repeatable: boolean;
7
+ /** Only meaningful in the outer `<name>.mod` file, ignored in descriptor.mod. */
8
+ outerOnly: boolean;
9
+ /** One-line label shown next to the completion item. */
10
+ summary: string;
11
+ /** Markdown: what the value means and what to put in. */
12
+ doc: string;
13
+ /** VS Code snippet inserted on completion (placeholder = example value). */
14
+ snippet: string;
15
+ }
16
+ export declare const DESCRIPTOR_FIELDS: DescriptorField[];
17
+ export declare const DESCRIPTOR_FIELD_MAP: ReadonlyMap<string, DescriptorField>;
18
+ /** The launcher's fixed tag categories (Mod_structure wiki page, launcher UI). */
19
+ export declare const LAUNCHER_TAGS: string[];
20
+ export interface DescriptorEntry {
21
+ key: string;
22
+ /** 0-based line of the key. */
23
+ line: number;
24
+ /** Column range of the key on its line. */
25
+ startCol: number;
26
+ endCol: number;
27
+ /** Raw text right of `=` (trimmed, quotes kept), "" when the value is a block. */
28
+ value: string;
29
+ }
30
+ /**
31
+ * Line-based parse of the flat key=value format. Only top-level keys are
32
+ * entries; lines inside a `{ }` block (tags, dependencies) are skipped.
33
+ */
34
+ export declare function parseDescriptor(text: string): DescriptorEntry[];
35
+ export interface DescriptorIssue {
36
+ code: "descriptor-missing-field" | "descriptor-unknown-key" | "descriptor-duplicate-key" | "descriptor-path-ignored";
37
+ severity: "error" | "warning";
38
+ line: number;
39
+ startCol: number;
40
+ endCol: number;
41
+ message: string;
42
+ }
43
+ /**
44
+ * Structural checks on a .mod file. Everything here is certain: the key set is
45
+ * closed (launcher docs) and required-ness is the launcher's own behavior.
46
+ */
47
+ export declare function validateDescriptor(text: string, opts: {
48
+ isDescriptorFile: boolean;
49
+ }): DescriptorIssue[];
50
+ /**
51
+ * The mod's display name from `<dir>/descriptor.mod` (`name="..."`), or null
52
+ * when the file or field is missing/unreadable. Lets UI surfaces say WHICH mod
53
+ * something comes from ("Community Flavor Pack") instead of a generic "mod".
54
+ */
55
+ export declare function readDescriptorName(dir: string): string | null;
56
+ /**
57
+ * The mod names inside `<dir>/descriptor.mod`'s `dependencies={ "A" "B" }`
58
+ * block, in file order; empty when the file or the block is missing. The
59
+ * launcher matches these against the other mods' `name=`, not against their
60
+ * Workshop id, so that is what the caller compares them with.
61
+ */
62
+ export declare function readDescriptorDependencies(dir: string): string[];
63
+ /** "1.19.0.6" -> "1.19.*" (the wildcard form that survives hotfixes). */
64
+ export declare function wildcardVersion(raw: string): string | null;
65
+ /** A launcher-correct starter descriptor.mod. */
66
+ export declare function scaffoldDescriptor(modName: string, supportedVersion: string): string;
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.LAUNCHER_TAGS = exports.DESCRIPTOR_FIELD_MAP = exports.DESCRIPTOR_FIELDS = void 0;
37
+ exports.parseDescriptor = parseDescriptor;
38
+ exports.validateDescriptor = validateDescriptor;
39
+ exports.readDescriptorName = readDescriptorName;
40
+ exports.readDescriptorDependencies = readDescriptorDependencies;
41
+ exports.wildcardVersion = wildcardVersion;
42
+ exports.scaffoldDescriptor = scaffoldDescriptor;
43
+ /**
44
+ * Knowledge table + validator for Paradox launcher `.mod` descriptor files
45
+ * (`descriptor.mod` inside the mod folder, `<name>.mod` next to it).
46
+ *
47
+ * The key set and tag list come from the official launcher docs
48
+ * (the Mod_structure page on the official wiki) cross-checked against 86 real .mod
49
+ * files (launcher-generated + Workshop). No vscode imports: unit-testable.
50
+ */
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ exports.DESCRIPTOR_FIELDS = [
54
+ {
55
+ key: "name",
56
+ required: true,
57
+ repeatable: false,
58
+ outerOnly: false,
59
+ summary: "Display name in the launcher and on Steam Workshop",
60
+ doc: "The name players see in the launcher's mod list and on the Workshop page. " +
61
+ "Needs at least 3 characters for a Workshop upload.\n\n" +
62
+ '```\nname="My Mod"\n```',
63
+ snippet: 'name="${1:My Mod}"',
64
+ },
65
+ {
66
+ key: "version",
67
+ required: true,
68
+ repeatable: false,
69
+ outerOnly: false,
70
+ summary: "Your mod's own version number (NOT the game version)",
71
+ doc: "Free-form version string shown in the launcher. Bump it when you release " +
72
+ "an update so players can tell versions apart. This is about your mod, " +
73
+ "not the game - the game version goes in `supported_version`.\n\n" +
74
+ '```\nversion="0.1.0"\n```',
75
+ snippet: 'version="${1:0.1.0}"',
76
+ },
77
+ {
78
+ key: "supported_version",
79
+ required: true,
80
+ repeatable: false,
81
+ outerOnly: false,
82
+ summary: "Newest game version the mod works with",
83
+ doc: "The launcher compares this against the installed game and marks the mod " +
84
+ "out of date when the game is newer. A `*` wildcard keeps the mod valid " +
85
+ "for every hotfix of a patch.\n\n" +
86
+ '```\nsupported_version="1.19.*"\n```',
87
+ snippet: 'supported_version="${1:1.19.*}"',
88
+ },
89
+ {
90
+ key: "tags",
91
+ required: false,
92
+ repeatable: false,
93
+ outerOnly: false,
94
+ summary: "Launcher / Workshop category tags (one quoted tag per line)",
95
+ doc: "Categories players can filter by in the launcher and on the Workshop. " +
96
+ "Pick from the launcher's list (completion inside the block offers all of " +
97
+ "them); a Workshop upload needs at least one.\n\n" +
98
+ '```\ntags={\n\t"Gameplay"\n\t"Events"\n}\n```',
99
+ snippet: 'tags={\n\t"${1:Gameplay}"\n}',
100
+ },
101
+ {
102
+ key: "path",
103
+ required: false,
104
+ repeatable: false,
105
+ outerOnly: true,
106
+ summary: "Where the mod folder is - outer <name>.mod file only",
107
+ doc: "Tells the launcher where the mod's files live. Absolute or relative to " +
108
+ "the game's user directory, forward slashes only.\n\n" +
109
+ "**Leave this line out of `descriptor.mod`** - it is ignored there, and a " +
110
+ "copied absolute path breaks when the mod is shared.\n\n" +
111
+ '```\npath="mod/my_mod"\n```',
112
+ snippet: 'path="${1:mod/my_mod}"',
113
+ },
114
+ {
115
+ key: "remote_file_id",
116
+ required: false,
117
+ repeatable: false,
118
+ outerOnly: false,
119
+ summary: "Steam Workshop item ID (set automatically on first upload)",
120
+ doc: "Links the local mod to its Workshop page so updates go to the same item. " +
121
+ "The launcher fills this in when you first upload - you only ever set it " +
122
+ "by hand to reconnect a mod to an existing Workshop item. Digits only.\n\n" +
123
+ '```\nremote_file_id="2962333032"\n```',
124
+ snippet: 'remote_file_id="${1:123456789}"',
125
+ },
126
+ {
127
+ key: "picture",
128
+ required: false,
129
+ repeatable: false,
130
+ outerOnly: false,
131
+ summary: "Launcher thumbnail image (file inside the mod folder)",
132
+ doc: "Image shown next to the mod in the launcher. Steam Workshop ignores it " +
133
+ "and always uses `thumbnail.png` in the mod root instead (1:1, max 1 MB).\n\n" +
134
+ '```\npicture="thumbnail.png"\n```',
135
+ snippet: 'picture="${1:thumbnail.png}"',
136
+ },
137
+ {
138
+ key: "replace_path",
139
+ required: false,
140
+ repeatable: true,
141
+ outerOnly: false,
142
+ summary: "Unload an entire vanilla folder (repeat per folder)",
143
+ doc: "The game skips every vanilla file under this folder, so only your mod's " +
144
+ "version of it exists. One line per folder, forward slashes, relative to " +
145
+ "the game root. Used by total conversions to drop vanilla history, " +
146
+ "titles, cultures etc. wholesale - do not use it for ordinary overrides.\n\n" +
147
+ '```\nreplace_path="history/characters"\nreplace_path="common/landed_titles"\n```',
148
+ snippet: 'replace_path="${1:history/characters}"',
149
+ },
150
+ {
151
+ key: "dependencies",
152
+ required: false,
153
+ repeatable: false,
154
+ outerOnly: false,
155
+ summary: "Mods that must load BEFORE this one",
156
+ doc: "The launcher sorts every listed mod above this one in the load order. " +
157
+ "Use the exact `name` of the other mod, one quoted name per line. Mostly " +
158
+ "for submods and compatibility patches.\n\n" +
159
+ '```\ndependencies={\n\t"A Game of Thrones"\n}\n```',
160
+ snippet: 'dependencies={\n\t"${1:Name of the parent mod}"\n}',
161
+ },
162
+ ];
163
+ exports.DESCRIPTOR_FIELD_MAP = new Map(exports.DESCRIPTOR_FIELDS.map((f) => [f.key, f]));
164
+ /** The launcher's fixed tag categories (Mod_structure wiki page, launcher UI). */
165
+ exports.LAUNCHER_TAGS = [
166
+ "Alternative History",
167
+ "Balance",
168
+ "Bookmarks",
169
+ "Character Focuses",
170
+ "Character Interactions",
171
+ "Culture",
172
+ "Decisions",
173
+ "Events",
174
+ "Fixes",
175
+ "Gameplay",
176
+ "Graphics",
177
+ "Historical",
178
+ "Map",
179
+ "Portraits",
180
+ "Religion",
181
+ "Schemes",
182
+ "Sound",
183
+ "Total Conversion",
184
+ "Translation",
185
+ "Utilities",
186
+ "Warfare",
187
+ ];
188
+ /**
189
+ * Line-based parse of the flat key=value format. Only top-level keys are
190
+ * entries; lines inside a `{ }` block (tags, dependencies) are skipped.
191
+ */
192
+ function parseDescriptor(text) {
193
+ const entries = [];
194
+ let depth = 0;
195
+ const lines = text.split(/\r?\n/);
196
+ for (let i = 0; i < lines.length; i++) {
197
+ const line = lines[i];
198
+ const noComment = line.replace(/#.*$/, "");
199
+ if (depth === 0) {
200
+ // Tolerate a UTF-8 BOM on the first line.
201
+ const m = /^(\uFEFF?\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(noComment);
202
+ if (m) {
203
+ const startCol = m[1].length;
204
+ entries.push({
205
+ key: m[2],
206
+ line: i,
207
+ startCol,
208
+ endCol: startCol + m[2].length,
209
+ value: m[3].trim().startsWith("{") ? "" : m[3].trim(),
210
+ });
211
+ }
212
+ }
213
+ for (const ch of noComment) {
214
+ if (ch === "{")
215
+ depth++;
216
+ else if (ch === "}")
217
+ depth = Math.max(0, depth - 1);
218
+ }
219
+ }
220
+ return entries;
221
+ }
222
+ /**
223
+ * Structural checks on a .mod file. Everything here is certain: the key set is
224
+ * closed (launcher docs) and required-ness is the launcher's own behavior.
225
+ */
226
+ function validateDescriptor(text, opts) {
227
+ const issues = [];
228
+ const entries = parseDescriptor(text);
229
+ const seen = new Map();
230
+ for (const e of entries) {
231
+ const field = exports.DESCRIPTOR_FIELD_MAP.get(e.key);
232
+ const at = { line: e.line, startCol: e.startCol, endCol: e.endCol };
233
+ if (!field) {
234
+ issues.push({
235
+ code: "descriptor-unknown-key",
236
+ severity: "warning",
237
+ ...at,
238
+ message: `'${e.key}' is not a .mod descriptor key; the launcher ignores it.`,
239
+ });
240
+ continue;
241
+ }
242
+ if (seen.has(e.key) && !field.repeatable) {
243
+ issues.push({
244
+ code: "descriptor-duplicate-key",
245
+ severity: "warning",
246
+ ...at,
247
+ message: `'${e.key}' appears more than once; only the last value counts.`,
248
+ });
249
+ }
250
+ seen.set(e.key, e);
251
+ if (e.key === "path" && opts.isDescriptorFile) {
252
+ issues.push({
253
+ code: "descriptor-path-ignored",
254
+ severity: "warning",
255
+ ...at,
256
+ message: "path= belongs in the outer <name>.mod file; inside descriptor.mod the launcher ignores it, " +
257
+ "and a machine-specific path leaks when the mod is shared.",
258
+ });
259
+ }
260
+ }
261
+ for (const field of exports.DESCRIPTOR_FIELDS) {
262
+ if (!field.required || seen.has(field.key))
263
+ continue;
264
+ // supported_version: the launcher still lists the mod, it just cannot
265
+ // check compatibility - a warning, not an error.
266
+ const isHard = field.key !== "supported_version";
267
+ issues.push({
268
+ code: "descriptor-missing-field",
269
+ severity: isHard ? "error" : "warning",
270
+ line: 0,
271
+ startCol: 0,
272
+ endCol: 200,
273
+ message: isHard
274
+ ? `Missing ${field.key}= - the launcher needs it to list the mod.`
275
+ : "Missing supported_version= - the launcher cannot tell which game version the mod is for.",
276
+ });
277
+ }
278
+ return issues;
279
+ }
280
+ /**
281
+ * The mod's display name from `<dir>/descriptor.mod` (`name="..."`), or null
282
+ * when the file or field is missing/unreadable. Lets UI surfaces say WHICH mod
283
+ * something comes from ("Community Flavor Pack") instead of a generic "mod".
284
+ */
285
+ function readDescriptorName(dir) {
286
+ let text;
287
+ try {
288
+ text = fs.readFileSync(path.join(dir, "descriptor.mod"), "utf8");
289
+ }
290
+ catch {
291
+ return null;
292
+ }
293
+ const entry = parseDescriptor(text).find((e) => e.key === "name");
294
+ if (!entry)
295
+ return null;
296
+ const value = entry.value.replace(/^"([^]*)"$/, "$1").trim();
297
+ return value === "" ? null : value;
298
+ }
299
+ /**
300
+ * The mod names inside `<dir>/descriptor.mod`'s `dependencies={ "A" "B" }`
301
+ * block, in file order; empty when the file or the block is missing. The
302
+ * launcher matches these against the other mods' `name=`, not against their
303
+ * Workshop id, so that is what the caller compares them with.
304
+ */
305
+ function readDescriptorDependencies(dir) {
306
+ let text;
307
+ try {
308
+ text = fs.readFileSync(path.join(dir, "descriptor.mod"), "utf8");
309
+ }
310
+ catch {
311
+ return [];
312
+ }
313
+ // Comments first: a commented-out dependency is not a dependency.
314
+ const block = /(?:^|\n)[ \t]*dependencies[ \t]*=[ \t]*\{([^}]*)\}/.exec(text.replace(/#[^\n]*/g, ""));
315
+ if (!block)
316
+ return [];
317
+ return [...block[1].matchAll(/"([^"]*)"/g)].map((m) => m[1].trim()).filter((s) => s !== "");
318
+ }
319
+ /** "1.19.0.6" -> "1.19.*" (the wildcard form that survives hotfixes). */
320
+ function wildcardVersion(raw) {
321
+ const m = /^(\d+)\.(\d+)/.exec(raw.trim());
322
+ return m ? `${m[1]}.${m[2]}.*` : null;
323
+ }
324
+ /** A launcher-correct starter descriptor.mod. */
325
+ function scaffoldDescriptor(modName, supportedVersion) {
326
+ return [
327
+ 'version="0.1.0"',
328
+ "tags={",
329
+ '\t"Gameplay"',
330
+ "}",
331
+ `name="${modName.replace(/"/g, "'")}"`,
332
+ `supported_version="${supportedVersion}"`,
333
+ "",
334
+ ].join("\n");
335
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Best-effort parsing of the game's logs/error.log lines. Pure (no vscode),
3
+ * so it stays unit-testable; the tailing/diagnostics wiring lives in the
4
+ * client.
5
+ */
6
+ export interface ParsedGameError {
7
+ message: string;
8
+ relFile: string;
9
+ /** 0-based, or null for file-level entries. */
10
+ line: number | null;
11
+ severity: "error" | "warning";
12
+ }
13
+ /** Parse one error.log line; null when it names no file. */
14
+ export declare function parseErrorLogLine(raw: string): ParsedGameError | null;
15
+ /**
16
+ * Stateful line parser: same as `parseErrorLogLine`, but additionally stitches
17
+ * multi-line `Script system error!` blocks together, where the actual error
18
+ * text and the file location sit on separate indented continuation lines:
19
+ *
20
+ * [18:14:55][E][jomini_script_system.cpp:303]: Script system error!
21
+ * Error: is_cultivator trigger [ Scoped object ... is not valid ]
22
+ * Script location: file: common/script_values/x.txt line: 25 (name)
23
+ *
24
+ * Line-by-line, the location line would become the diagnostic message and the
25
+ * error text would be dropped. Feed EVERY line through `push` in order (state
26
+ * carries across reads); call `reset` when the log is cleared or replaced.
27
+ */
28
+ export declare class ErrorLogParser {
29
+ private pendingSeverity;
30
+ private pendingError;
31
+ push(raw: string): ParsedGameError | null;
32
+ reset(): void;
33
+ }