@px-lsp/protocol 0.1.0 → 0.2.1

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 CHANGED
@@ -1,25 +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.
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,71 @@
1
+ /**
2
+ * Custom calendar display: total-conversion mods (AGoT, LotR, Hegemonia...)
3
+ * keep script dates on the engine's single increasing year axis but *display*
4
+ * them on their own era system, e.g. epoch 4000 means script year 3000 shows
5
+ * as "1000 BC" and 4000 as "1 AD". The mapping cannot be detected from mod
6
+ * files, so the mod declares it once in `px.calendar` (workspace settings,
7
+ * committed with the mod).
8
+ *
9
+ * Pure logic, no `vscode` imports: shared by the language server (inlay hints,
10
+ * hover) and the extension (the Insert Date command), unit-tested in plain
11
+ * Node.
12
+ */
13
+ export interface CalendarSetting {
14
+ /** Script year displayed as year 1 of the `after` era (no year zero). */
15
+ epoch: number;
16
+ /** Era label for script years >= epoch ("AD", "TA"...). */
17
+ after: string;
18
+ /** Era label for script years < epoch ("BC"). Omitted = single-era
19
+ * calendar: years before the epoch get no display form. */
20
+ before?: string;
21
+ /**
22
+ * The engine's twelve months under the mod's own names, first month first.
23
+ * Omitted = January to December. Only the NAMES are the mod's: the game has
24
+ * twelve months of fixed length (31 28 31 30 31 30 31 31 30 31 30 31, no
25
+ * leap years) and no script can change that, so a date's month and day are
26
+ * always the engine's and only read differently.
27
+ */
28
+ months?: string[];
29
+ }
30
+ /** Days per engine month; what a script date's day is bounded by. */
31
+ export declare const ENGINE_MONTH_DAYS: number[];
32
+ export declare const GREGORIAN_MONTHS: string[];
33
+ /** The month names a date reads with: the mod's twelve, or the engine's. */
34
+ export declare function monthNames(cal: CalendarSetting): string[];
35
+ /**
36
+ * Validate a calendar straight out of JSON settings (any client, any hand-
37
+ * edited settings file). Returns a clean copy, or undefined when the value is
38
+ * not a usable calendar - features then behave as if none was configured.
39
+ */
40
+ export declare function sanitizeCalendar(raw: unknown): CalendarSetting | undefined;
41
+ /** A script-file date token (`3000.1.1`), or null when the text is not one. */
42
+ export declare function parseScriptDate(text: string): {
43
+ y: number;
44
+ m: number;
45
+ d: number;
46
+ } | null;
47
+ /** A date the engine reads: a positive year, one of its twelve months, a day that month has. */
48
+ export declare function isValidScriptDate(y: number, m: number, d: number): boolean;
49
+ /** Era-mapped year: "1000 BC". Null for pre-epoch years of a single-era calendar. */
50
+ export declare function displayYear(cal: CalendarSetting, y: number): string | null;
51
+ /**
52
+ * Full display form of a script date: "1000 BC" for the year's first day
53
+ * (how start dates read in game), "15 March 1000 BC" otherwise. Null when the
54
+ * date does not fit the calendar.
55
+ */
56
+ export declare function displayDate(cal: CalendarSetting, y: number, m: number, d: number): string | null;
57
+ export type ConvertResult = {
58
+ ok: true;
59
+ script: string;
60
+ display: string;
61
+ } | {
62
+ ok: false;
63
+ error: string;
64
+ };
65
+ /**
66
+ * A display-calendar date typed by the user -> the script date the file needs.
67
+ * Grammar: `YEAR [ERA] [MONTH [DAY]]`, e.g. "1000 BC", "1000 BC March 15",
68
+ * "1000 BC 3 15", "1 AD", "500" (era defaults to `after`). MONTH is a number
69
+ * or a month name (unique prefix is enough).
70
+ */
71
+ export declare function convertDisplayInput(cal: CalendarSetting, input: string): ConvertResult;
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * Custom calendar display: total-conversion mods (AGoT, LotR, Hegemonia...)
4
+ * keep script dates on the engine's single increasing year axis but *display*
5
+ * them on their own era system, e.g. epoch 4000 means script year 3000 shows
6
+ * as "1000 BC" and 4000 as "1 AD". The mapping cannot be detected from mod
7
+ * files, so the mod declares it once in `px.calendar` (workspace settings,
8
+ * committed with the mod).
9
+ *
10
+ * Pure logic, no `vscode` imports: shared by the language server (inlay hints,
11
+ * hover) and the extension (the Insert Date command), unit-tested in plain
12
+ * Node.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.GREGORIAN_MONTHS = exports.ENGINE_MONTH_DAYS = void 0;
16
+ exports.monthNames = monthNames;
17
+ exports.sanitizeCalendar = sanitizeCalendar;
18
+ exports.parseScriptDate = parseScriptDate;
19
+ exports.isValidScriptDate = isValidScriptDate;
20
+ exports.displayYear = displayYear;
21
+ exports.displayDate = displayDate;
22
+ exports.convertDisplayInput = convertDisplayInput;
23
+ /** Days per engine month; what a script date's day is bounded by. */
24
+ exports.ENGINE_MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
25
+ exports.GREGORIAN_MONTHS = [
26
+ "January",
27
+ "February",
28
+ "March",
29
+ "April",
30
+ "May",
31
+ "June",
32
+ "July",
33
+ "August",
34
+ "September",
35
+ "October",
36
+ "November",
37
+ "December",
38
+ ];
39
+ /** The month names a date reads with: the mod's twelve, or the engine's. */
40
+ function monthNames(cal) {
41
+ return cal.months ?? exports.GREGORIAN_MONTHS;
42
+ }
43
+ /**
44
+ * Validate a calendar straight out of JSON settings (any client, any hand-
45
+ * edited settings file). Returns a clean copy, or undefined when the value is
46
+ * not a usable calendar - features then behave as if none was configured.
47
+ */
48
+ function sanitizeCalendar(raw) {
49
+ if (typeof raw !== "object" || raw === null)
50
+ return undefined;
51
+ const o = raw;
52
+ const epoch = o.epoch;
53
+ const after = o.after;
54
+ if (typeof epoch !== "number" || !Number.isInteger(epoch) || epoch < 1)
55
+ return undefined;
56
+ if (typeof after !== "string" || after.trim() === "")
57
+ return undefined;
58
+ const cal = { epoch, after: after.trim() };
59
+ if (typeof o.before === "string" && o.before.trim() !== "")
60
+ cal.before = o.before.trim();
61
+ // Typed input picks the era by its label and the month by its name, so a
62
+ // collision would resolve silently to the wrong one: not a usable calendar.
63
+ if (cal.before && cal.before.toLowerCase() === cal.after.toLowerCase())
64
+ return undefined;
65
+ if (Array.isArray(o.months) && o.months.length > 0) {
66
+ // Exactly the engine's twelve. A name may still arrive as the older
67
+ // `{ name, days }` object; its day count never meant anything to the
68
+ // game and is dropped.
69
+ if (o.months.length !== exports.GREGORIAN_MONTHS.length)
70
+ return undefined;
71
+ const months = [];
72
+ const seen = new Set();
73
+ for (const m of o.months) {
74
+ const name = typeof m === "string" ? m : (m?.name ?? null);
75
+ if (typeof name !== "string" || name.trim() === "")
76
+ return undefined;
77
+ const key = name.trim().toLowerCase();
78
+ if (seen.has(key))
79
+ return undefined;
80
+ seen.add(key);
81
+ months.push(name.trim());
82
+ }
83
+ cal.months = months;
84
+ }
85
+ return cal;
86
+ }
87
+ /** A script-file date token (`3000.1.1`), or null when the text is not one. */
88
+ function parseScriptDate(text) {
89
+ const match = /^(\d{1,5})\.(\d{1,2})\.(\d{1,2})$/.exec(text);
90
+ if (!match)
91
+ return null;
92
+ return { y: Number(match[1]), m: Number(match[2]), d: Number(match[3]) };
93
+ }
94
+ /** A date the engine reads: a positive year, one of its twelve months, a day that month has. */
95
+ function isValidScriptDate(y, m, d) {
96
+ return y >= 1 && m >= 1 && m <= exports.ENGINE_MONTH_DAYS.length && d >= 1 && d <= exports.ENGINE_MONTH_DAYS[m - 1];
97
+ }
98
+ /** Era-mapped year: "1000 BC". Null for pre-epoch years of a single-era calendar. */
99
+ function displayYear(cal, y) {
100
+ if (y >= cal.epoch)
101
+ return `${y - cal.epoch + 1} ${cal.after}`;
102
+ return cal.before ? `${cal.epoch - y} ${cal.before}` : null;
103
+ }
104
+ /**
105
+ * Full display form of a script date: "1000 BC" for the year's first day
106
+ * (how start dates read in game), "15 March 1000 BC" otherwise. Null when the
107
+ * date does not fit the calendar.
108
+ */
109
+ function displayDate(cal, y, m, d) {
110
+ if (!isValidScriptDate(y, m, d))
111
+ return null;
112
+ const year = displayYear(cal, y);
113
+ if (!year)
114
+ return null;
115
+ if (m === 1 && d === 1)
116
+ return year;
117
+ return `${d} ${monthNames(cal)[m - 1]} ${year}`;
118
+ }
119
+ /** Case-insensitive month lookup: exact name, else unique prefix. */
120
+ function monthByName(cal, text) {
121
+ const needle = text.toLowerCase();
122
+ const months = monthNames(cal);
123
+ const exact = months.findIndex((m) => m.toLowerCase() === needle);
124
+ if (exact >= 0)
125
+ return exact + 1;
126
+ const prefixed = months.map((m, i) => ({ m, i })).filter(({ m }) => m.toLowerCase().startsWith(needle));
127
+ return prefixed.length === 1 ? prefixed[0].i + 1 : null;
128
+ }
129
+ /**
130
+ * A display-calendar date typed by the user -> the script date the file needs.
131
+ * Grammar: `YEAR [ERA] [MONTH [DAY]]`, e.g. "1000 BC", "1000 BC March 15",
132
+ * "1000 BC 3 15", "1 AD", "500" (era defaults to `after`). MONTH is a number
133
+ * or a month name (unique prefix is enough).
134
+ */
135
+ function convertDisplayInput(cal, input) {
136
+ const words = input.trim().split(/\s+/).filter(Boolean);
137
+ if (words.length === 0)
138
+ return { ok: false, error: "type a year, e.g. 1000 " + (cal.before ?? cal.after) };
139
+ if (!/^\d{1,5}$/.test(words[0]))
140
+ return { ok: false, error: `"${words[0]}" is not a year` };
141
+ const year = Number(words[0]);
142
+ let rest = words.slice(1);
143
+ let era = cal.after;
144
+ if (rest.length > 0) {
145
+ const w = rest[0].toLowerCase();
146
+ if (w === cal.after.toLowerCase()) {
147
+ rest = rest.slice(1);
148
+ }
149
+ else if (cal.before && w === cal.before.toLowerCase()) {
150
+ era = cal.before;
151
+ rest = rest.slice(1);
152
+ }
153
+ }
154
+ let m = 1;
155
+ let d = 1;
156
+ if (rest.length > 0) {
157
+ const month = /^\d{1,2}$/.test(rest[0]) ? Number(rest[0]) : monthByName(cal, rest[0]);
158
+ if (month === null)
159
+ return { ok: false, error: `"${rest[0]}" is not a month of this calendar` };
160
+ m = month;
161
+ rest = rest.slice(1);
162
+ }
163
+ if (rest.length > 0) {
164
+ if (!/^\d{1,3}$/.test(rest[0]))
165
+ return { ok: false, error: `"${rest[0]}" is not a day` };
166
+ d = Number(rest[0]);
167
+ rest = rest.slice(1);
168
+ }
169
+ if (rest.length > 0)
170
+ return { ok: false, error: `unexpected "${rest.join(" ")}"` };
171
+ if (year < 1)
172
+ return { ok: false, error: "years start at 1 (no year zero)" };
173
+ const y = era === cal.after ? year + cal.epoch - 1 : cal.epoch - year;
174
+ if (y < 1)
175
+ return { ok: false, error: `${year} ${era} is before script year 1 (epoch ${cal.epoch})` };
176
+ if (!isValidScriptDate(y, m, d)) {
177
+ return m >= 1 && m <= exports.ENGINE_MONTH_DAYS.length
178
+ ? { ok: false, error: `${monthNames(cal)[m - 1]} has ${exports.ENGINE_MONTH_DAYS[m - 1]} days` }
179
+ : { ok: false, error: `the game has ${exports.ENGINE_MONTH_DAYS.length} months` };
180
+ }
181
+ return { ok: true, script: `${y}.${m}.${d}`, display: displayDate(cal, y, m, d) };
182
+ }
@@ -0,0 +1,26 @@
1
+ import { type CalendarSetting } from "./calendar";
2
+ import { type ConfigDirNames } from "./configDir";
3
+ export declare const CALENDAR_FILE = "calendar.json";
4
+ export interface CalendarFile {
5
+ /** Where the declaration was read from (or would be written to). */
6
+ file: string;
7
+ /** The declared calendar, when the file parses and sanitizes. */
8
+ calendar?: CalendarSetting;
9
+ /** Why an existing file yields no calendar: unparsable JSON or an unusable shape. */
10
+ error?: string;
11
+ }
12
+ /** The path the file is read from: the mod's config dir (legacy name included). */
13
+ export declare function calendarFilePath(modRoot: string, names: ConfigDirNames): string;
14
+ /**
15
+ * Read `<mod>/.px-toolkit/calendar.json`. Null when the file does not exist;
16
+ * a `CalendarFile` without `calendar` when it exists but is not usable, so a
17
+ * client can say so instead of silently showing no dates.
18
+ */
19
+ export declare function readCalendarFile(modRoot: string, names: ConfigDirNames): CalendarFile | null;
20
+ /**
21
+ * Write the declaration into the mod (renaming a legacy config dir first,
22
+ * like every other config-dir write). Returns the file path.
23
+ */
24
+ export declare function writeCalendarFile(modRoot: string, names: ConfigDirNames, cal: CalendarSetting): string;
25
+ /** True when `fsPath` is a calendar declaration file (any config dir name). */
26
+ export declare function isCalendarFile(fsPath: string, names: ConfigDirNames): boolean;
@@ -0,0 +1,109 @@
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.CALENDAR_FILE = void 0;
37
+ exports.calendarFilePath = calendarFilePath;
38
+ exports.readCalendarFile = readCalendarFile;
39
+ exports.writeCalendarFile = writeCalendarFile;
40
+ exports.isCalendarFile = isCalendarFile;
41
+ /**
42
+ * The per-mod calendar declaration: `<mod>/.px-toolkit/calendar.json`, the
43
+ * JSON form of calendar.ts `CalendarSetting`. A display calendar is a fact
44
+ * about the mod, so it travels with the mod (committed, one per mod, read by
45
+ * every client and by the server itself) instead of living in one editor's
46
+ * window-scoped `px.calendar` setting. The setting stays as the fallback for
47
+ * a mod without the file.
48
+ *
49
+ * No `vscode` imports: unit-tested in plain Node.
50
+ */
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const calendar_1 = require("./calendar");
54
+ const configDir_1 = require("./configDir");
55
+ exports.CALENDAR_FILE = "calendar.json";
56
+ /** The path the file is read from: the mod's config dir (legacy name included). */
57
+ function calendarFilePath(modRoot, names) {
58
+ return path.join((0, configDir_1.resolveConfigDir)(modRoot, names), exports.CALENDAR_FILE);
59
+ }
60
+ /**
61
+ * Read `<mod>/.px-toolkit/calendar.json`. Null when the file does not exist;
62
+ * a `CalendarFile` without `calendar` when it exists but is not usable, so a
63
+ * client can say so instead of silently showing no dates.
64
+ */
65
+ function readCalendarFile(modRoot, names) {
66
+ const file = calendarFilePath(modRoot, names);
67
+ let text;
68
+ try {
69
+ text = fs.readFileSync(file, "utf8");
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ let raw;
75
+ try {
76
+ raw = JSON.parse(text.replace(/^\uFEFF/, ""));
77
+ }
78
+ catch (err) {
79
+ return { file, error: `not valid JSON (${err.message})` };
80
+ }
81
+ const calendar = (0, calendar_1.sanitizeCalendar)(raw);
82
+ if (!calendar) {
83
+ return {
84
+ file,
85
+ error: 'not a usable calendar: needs a whole-number "epoch" (1 or more), a non-empty "after" era label, ' +
86
+ 'a "before" label different from "after" when present, and, when "months" is given, exactly twelve distinct names',
87
+ };
88
+ }
89
+ return { file, calendar };
90
+ }
91
+ /**
92
+ * Write the declaration into the mod (renaming a legacy config dir first,
93
+ * like every other config-dir write). Returns the file path.
94
+ */
95
+ function writeCalendarFile(modRoot, names, cal) {
96
+ const dir = (0, configDir_1.migrateConfigDir)(modRoot, names);
97
+ fs.mkdirSync(dir, { recursive: true });
98
+ const file = path.join(dir, exports.CALENDAR_FILE);
99
+ fs.writeFileSync(file, JSON.stringify(cal, null, 2) + "\n", "utf8");
100
+ return file;
101
+ }
102
+ /** True when `fsPath` is a calendar declaration file (any config dir name). */
103
+ function isCalendarFile(fsPath, names) {
104
+ const parts = fsPath.split(/[\\/]/);
105
+ if (parts.length < 2 || parts[parts.length - 1].toLowerCase() !== exports.CALENDAR_FILE)
106
+ return false;
107
+ const dir = parts[parts.length - 2].toLowerCase();
108
+ return dir === names.configDirName.toLowerCase() || dir === names.legacyConfigDirName?.toLowerCase();
109
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Generate the GAME-SIDE localization for a `px.calendar` display calendar:
3
+ * the era-math datafunction keys plus the `localization/replace/` overrides
4
+ * of the engine's date-format and month-name keys. This is the mirror image
5
+ * of calendar.ts: that file teaches the EDITOR the mapping, this one writes
6
+ * the mapping into the mod so the GAME displays it (the AGoT/LotR technique).
7
+ *
8
+ * Pure logic, no `vscode` imports; the extension command writes the returned
9
+ * files (adding the BOM) and per-game key names come in via `CalendarLocSpec`
10
+ * from the GameProfile, so no game knowledge lives here.
11
+ */
12
+ import type { CalendarSetting } from "./calendar";
13
+ /**
14
+ * The loc keys a game formats dates through, verified against the game's
15
+ * binary/files. Lives in the GameProfile (`games/<id>/meta.ts`); a profile
16
+ * without one does not support generation.
17
+ */
18
+ export interface CalendarLocSpec {
19
+ /**
20
+ * Vanilla date-format keys to override in `localization/replace/`, mapped
21
+ * to their format template. `{year}` and `{era}` mark where the generated
22
+ * era-math key references go; everything else is kept verbatim (the
23
+ * vanilla `$DAY$`/`$MONTH$` parameters).
24
+ */
25
+ dateFormats: Record<string, string>;
26
+ /**
27
+ * Engine month-name loc keys, first month first: [longKey, shortKey].
28
+ * Overridden only when the calendar declares custom months, and only when
29
+ * it declares exactly as many months as the engine has.
30
+ */
31
+ monthKeys?: [string, string][];
32
+ }
33
+ /** One file the generator wants written, path relative to the mod root. */
34
+ export interface GeneratedLocFile {
35
+ /** Forward slashes; the writer joins it onto the mod root. */
36
+ relPath: string;
37
+ /** Content without BOM; the writer prepends it. */
38
+ content: string;
39
+ }
40
+ export interface CalendarLocResult {
41
+ files: GeneratedLocFile[];
42
+ /** Facts the modder should know after generating (limits, follow-ups). */
43
+ notes: string[];
44
+ }
45
+ /** Keys the generated files define; exported so tests and docs stay in sync. */
46
+ export declare const CAL_YEAR_KEY = "PX_CAL_YEAR";
47
+ export declare const CAL_ERA_KEY = "PX_CAL_ERA";
48
+ /**
49
+ * The two files implementing the calendar in game:
50
+ *
51
+ * - `localization/<lang>/px_calendar_l_<lang>.yml` - the era-math keys
52
+ * (new keys, so NOT in replace/).
53
+ * - `localization/replace/<lang>/px_calendar_dates_l_<lang>.yml` - the
54
+ * vanilla date-format overrides routing `$YEAR$` through them, plus the
55
+ * engine month-name overrides when the calendar has custom months.
56
+ *
57
+ * Deterministic filenames: regenerating after a `px.calendar` change
58
+ * overwrites the same two files.
59
+ */
60
+ export declare function generateCalendarLoc(cal: CalendarSetting, spec: CalendarLocSpec, lang: string): CalendarLocResult;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CAL_ERA_KEY = exports.CAL_YEAR_KEY = void 0;
4
+ exports.generateCalendarLoc = generateCalendarLoc;
5
+ /** Keys the generated files define; exported so tests and docs stay in sync. */
6
+ exports.CAL_YEAR_KEY = "PX_CAL_YEAR";
7
+ exports.CAL_ERA_KEY = "PX_CAL_ERA";
8
+ /** Era labels sit inside '...' CString literals; a quote would break the
9
+ * datafunction silently, which is exactly the failure class we exist to
10
+ * prevent. */
11
+ function cstring(label) {
12
+ return label.replace(/'/g, "");
13
+ }
14
+ /**
15
+ * `[Select_int32(...)]` era math for the year, mirroring
16
+ * calendar.ts `displayYear`: y >= epoch shows as y - (epoch - 1), below as
17
+ * epoch - y. A single-era calendar (no `before`) clamps to 1 instead, so
18
+ * pre-epoch years never display as 0 or negative.
19
+ */
20
+ function yearExpr(cal) {
21
+ const year = "'(int32)$YEAR|q$'";
22
+ const toAfter = `Subtract_int32( ${year}, '(int32)${cal.epoch - 1}' )`;
23
+ if (!cal.before)
24
+ return `[Max_int32( ${toAfter}, '(int32)1' )]`;
25
+ const toBefore = `Subtract_int32( '(int32)${cal.epoch}', ${year} )`;
26
+ return `[Select_int32( GreaterThanOrEqualTo_int32( ${year}, '(int32)${cal.epoch}' ), ${toAfter}, ${toBefore} )]`;
27
+ }
28
+ function eraExpr(cal) {
29
+ if (!cal.before)
30
+ return cal.after;
31
+ return (`[Select_CString( GreaterThanOrEqualTo_int32( '(int32)$YEAR|q$', '(int32)${cal.epoch}' ), ` +
32
+ `'${cstring(cal.after)}', '${cstring(cal.before)}' )]`);
33
+ }
34
+ function locFile(lang, lines) {
35
+ return [`l_${lang}:`, ...lines, ""].join("\n");
36
+ }
37
+ /**
38
+ * The two files implementing the calendar in game:
39
+ *
40
+ * - `localization/<lang>/px_calendar_l_<lang>.yml` - the era-math keys
41
+ * (new keys, so NOT in replace/).
42
+ * - `localization/replace/<lang>/px_calendar_dates_l_<lang>.yml` - the
43
+ * vanilla date-format overrides routing `$YEAR$` through them, plus the
44
+ * engine month-name overrides when the calendar has custom months.
45
+ *
46
+ * Deterministic filenames: regenerating after a `px.calendar` change
47
+ * overwrites the same two files.
48
+ */
49
+ function generateCalendarLoc(cal, spec, lang) {
50
+ const notes = [];
51
+ const header = [
52
+ " # Generated by 'Paradox: Generate Calendar Localization' from the px.calendar",
53
+ " # setting. Edit px.calendar and regenerate instead of editing by hand.",
54
+ ];
55
+ const eraDoc = cal.before
56
+ ? ` # Script year >= ${cal.epoch} shows as (year - ${cal.epoch - 1}) ${cal.after}; below as (${cal.epoch} - year) ${cal.before}.`
57
+ : ` # Script year >= ${cal.epoch} shows as (year - ${cal.epoch - 1}) ${cal.after}; earlier years clamp to 1.`;
58
+ const mathLines = [
59
+ ...header,
60
+ eraDoc,
61
+ ` ${exports.CAL_YEAR_KEY}:0 "${yearExpr(cal)}"`,
62
+ ` ${exports.CAL_ERA_KEY}:0 "${eraExpr(cal)}"`,
63
+ ];
64
+ const overrideLines = [
65
+ ...header,
66
+ " # Overrides of the engine's date-format keys: same formats as vanilla, with",
67
+ ` # the raw $YEAR$ routed through ${exports.CAL_YEAR_KEY}/${exports.CAL_ERA_KEY}.`,
68
+ ];
69
+ for (const [key, format] of Object.entries(spec.dateFormats)) {
70
+ const value = format.replace("{year}", `$${exports.CAL_YEAR_KEY}$`).replace("{era}", `$${exports.CAL_ERA_KEY}$`);
71
+ overrideLines.push(` ${key}:0 "${value}"`);
72
+ }
73
+ if (cal.months) {
74
+ const months = cal.months;
75
+ if (!spec.monthKeys) {
76
+ notes.push("This game's month-name keys are not mapped yet; custom month names were not generated.");
77
+ }
78
+ else {
79
+ // The calendar carries the engine's twelve names (sanitizeCalendar); the
80
+ // spec lists the keys of the months it knows, first month first.
81
+ overrideLines.push(" # Engine month names (long and abbreviated forms both get the custom name).");
82
+ spec.monthKeys.forEach(([long, short], i) => {
83
+ const name = months[i].replace(/"/g, '\\"');
84
+ overrideLines.push(` ${long}:0 "${name}"`);
85
+ if (short !== long)
86
+ overrideLines.push(` ${short}:0 "${name}"`);
87
+ });
88
+ }
89
+ }
90
+ notes.push(`Generated for '${lang}' only; other languages the mod ships need the same overrides in their replace folder.`);
91
+ return {
92
+ files: [
93
+ { relPath: `localization/${lang}/px_calendar_l_${lang}.yml`, content: locFile(lang, mathLines) },
94
+ {
95
+ relPath: `localization/replace/${lang}/px_calendar_dates_l_${lang}.yml`,
96
+ content: locFile(lang, overrideLines),
97
+ },
98
+ ],
99
+ notes,
100
+ };
101
+ }
@@ -0,0 +1,17 @@
1
+ export declare const PX_CONFIG_DIR = ".px-toolkit";
2
+ export interface ConfigDirNames {
3
+ configDirName: string;
4
+ /** The pre-0.4.0 per-game name, still read as a fallback. */
5
+ legacyConfigDirName?: string;
6
+ }
7
+ /**
8
+ * The config dir to READ from: the current name when it exists, else the
9
+ * legacy one when that exists, else the current name. Never touches disk.
10
+ */
11
+ export declare function resolveConfigDir(root: string, names: ConfigDirNames): string;
12
+ /**
13
+ * The config dir to WRITE to. Renames a legacy dir to the current name first;
14
+ * if the rename fails (locked file, read-only parent) the legacy dir stays in
15
+ * use so the write still lands where reads look.
16
+ */
17
+ export declare function migrateConfigDir(root: string, names: ConfigDirNames): string;