@px-lsp/protocol 0.1.0 → 0.2.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/LICENSE +674 -674
- package/README.md +25 -25
- package/dist/calendar.d.ts +69 -0
- package/dist/calendar.js +183 -0
- package/dist/calendarLoc.d.ts +60 -0
- package/dist/calendarLoc.js +105 -0
- package/dist/descriptorMod.d.ts +21 -0
- package/dist/descriptorMod.js +61 -5
- package/dist/kinds.d.ts +72 -0
- package/dist/kinds.js +186 -0
- package/dist/protocol.d.ts +180 -4
- package/dist/protocol.js +37 -2
- package/dist/workshopMeta.d.ts +42 -0
- package/dist/workshopMeta.js +151 -0
- package/package.json +1 -1
- package/src/arrays.ts +16 -16
- package/src/calendar.ts +184 -0
- package/src/calendarLoc.ts +166 -0
- package/src/constants.ts +12 -12
- package/src/descriptorMetadata.ts +101 -101
- package/src/descriptorMod.ts +414 -354
- package/src/errorLogParser.ts +136 -136
- package/src/fsWalk.ts +126 -126
- package/src/kinds.ts +205 -0
- package/src/locProperties.ts +43 -43
- package/src/locRefs.ts +38 -38
- package/src/modName.ts +18 -18
- package/src/protocol.ts +1669 -1459
- package/src/regex.ts +19 -19
- package/src/suppression.ts +178 -178
- package/src/tigerParser.ts +79 -79
- package/src/translationCore.ts +140 -140
- package/src/types.ts +90 -90
- package/src/workshopMeta.ts +132 -0
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,69 @@
|
|
|
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 CalendarMonth {
|
|
14
|
+
/** Display name ("March", "Narvinye"). */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Day count; the engine has no leap years, so one number per month. */
|
|
17
|
+
days: number;
|
|
18
|
+
}
|
|
19
|
+
export interface CalendarSetting {
|
|
20
|
+
/** Script year displayed as year 1 of the `after` era (no year zero). */
|
|
21
|
+
epoch: number;
|
|
22
|
+
/** Era label for script years >= epoch ("AD", "TA"...). */
|
|
23
|
+
after: string;
|
|
24
|
+
/** Era label for script years < epoch ("BC"). Omitted = single-era
|
|
25
|
+
* calendar: years before the epoch get no display form. */
|
|
26
|
+
before?: string;
|
|
27
|
+
/** Custom month names and day counts, first month first. Omitted = the
|
|
28
|
+
* standard 12 months (Feb 28: the engine has no leap years). */
|
|
29
|
+
months?: CalendarMonth[];
|
|
30
|
+
}
|
|
31
|
+
export declare const GREGORIAN_MONTHS: CalendarMonth[];
|
|
32
|
+
export declare function monthsOf(cal: CalendarSetting): CalendarMonth[];
|
|
33
|
+
/**
|
|
34
|
+
* Validate a calendar straight out of JSON settings (any client, any hand-
|
|
35
|
+
* edited settings file). Returns a clean copy, or undefined when the value is
|
|
36
|
+
* not a usable calendar - features then behave as if none was configured.
|
|
37
|
+
*/
|
|
38
|
+
export declare function sanitizeCalendar(raw: unknown): CalendarSetting | undefined;
|
|
39
|
+
/** A script-file date token (`3000.1.1`), or null when the text is not one. */
|
|
40
|
+
export declare function parseScriptDate(text: string): {
|
|
41
|
+
y: number;
|
|
42
|
+
m: number;
|
|
43
|
+
d: number;
|
|
44
|
+
} | null;
|
|
45
|
+
/** Month/day within the calendar's bounds (year just has to be positive). */
|
|
46
|
+
export declare function isValidScriptDate(cal: CalendarSetting, y: number, m: number, d: number): boolean;
|
|
47
|
+
/** Era-mapped year: "1000 BC". Null for pre-epoch years of a single-era calendar. */
|
|
48
|
+
export declare function displayYear(cal: CalendarSetting, y: number): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Full display form of a script date: "1000 BC" for the year's first day
|
|
51
|
+
* (how start dates read in game), "15 March 1000 BC" otherwise. Null when the
|
|
52
|
+
* date does not fit the calendar.
|
|
53
|
+
*/
|
|
54
|
+
export declare function displayDate(cal: CalendarSetting, y: number, m: number, d: number): string | null;
|
|
55
|
+
export type ConvertResult = {
|
|
56
|
+
ok: true;
|
|
57
|
+
script: string;
|
|
58
|
+
display: string;
|
|
59
|
+
} | {
|
|
60
|
+
ok: false;
|
|
61
|
+
error: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* A display-calendar date typed by the user -> the script date the file needs.
|
|
65
|
+
* Grammar: `YEAR [ERA] [MONTH [DAY]]`, e.g. "1000 BC", "1000 BC March 15",
|
|
66
|
+
* "1000 BC 3 15", "1 AD", "500" (era defaults to `after`). MONTH is a number
|
|
67
|
+
* or a month name (unique prefix is enough).
|
|
68
|
+
*/
|
|
69
|
+
export declare function convertDisplayInput(cal: CalendarSetting, input: string): ConvertResult;
|
package/dist/calendar.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
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 = void 0;
|
|
16
|
+
exports.monthsOf = monthsOf;
|
|
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
|
+
const DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
24
|
+
exports.GREGORIAN_MONTHS = [
|
|
25
|
+
"January",
|
|
26
|
+
"February",
|
|
27
|
+
"March",
|
|
28
|
+
"April",
|
|
29
|
+
"May",
|
|
30
|
+
"June",
|
|
31
|
+
"July",
|
|
32
|
+
"August",
|
|
33
|
+
"September",
|
|
34
|
+
"October",
|
|
35
|
+
"November",
|
|
36
|
+
"December",
|
|
37
|
+
].map((name, i) => ({ name, days: DAYS[i] }));
|
|
38
|
+
function monthsOf(cal) {
|
|
39
|
+
return cal.months ?? exports.GREGORIAN_MONTHS;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate a calendar straight out of JSON settings (any client, any hand-
|
|
43
|
+
* edited settings file). Returns a clean copy, or undefined when the value is
|
|
44
|
+
* not a usable calendar - features then behave as if none was configured.
|
|
45
|
+
*/
|
|
46
|
+
function sanitizeCalendar(raw) {
|
|
47
|
+
if (typeof raw !== "object" || raw === null)
|
|
48
|
+
return undefined;
|
|
49
|
+
const o = raw;
|
|
50
|
+
const epoch = o.epoch;
|
|
51
|
+
const after = o.after;
|
|
52
|
+
if (typeof epoch !== "number" || !Number.isInteger(epoch) || epoch < 1)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (typeof after !== "string" || after.trim() === "")
|
|
55
|
+
return undefined;
|
|
56
|
+
const cal = { epoch, after: after.trim() };
|
|
57
|
+
if (typeof o.before === "string" && o.before.trim() !== "")
|
|
58
|
+
cal.before = o.before.trim();
|
|
59
|
+
// Typed input picks the era by its label and the month by its name, so a
|
|
60
|
+
// collision would resolve silently to the wrong one: not a usable calendar.
|
|
61
|
+
if (cal.before && cal.before.toLowerCase() === cal.after.toLowerCase())
|
|
62
|
+
return undefined;
|
|
63
|
+
if (Array.isArray(o.months) && o.months.length > 0) {
|
|
64
|
+
const months = [];
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
for (const m of o.months) {
|
|
67
|
+
if (typeof m !== "object" || m === null)
|
|
68
|
+
return undefined;
|
|
69
|
+
const { name, days } = m;
|
|
70
|
+
if (typeof name !== "string" || name.trim() === "")
|
|
71
|
+
return undefined;
|
|
72
|
+
if (typeof days !== "number" || !Number.isInteger(days) || days < 1 || days > 999)
|
|
73
|
+
return undefined;
|
|
74
|
+
const key = name.trim().toLowerCase();
|
|
75
|
+
if (seen.has(key))
|
|
76
|
+
return undefined;
|
|
77
|
+
seen.add(key);
|
|
78
|
+
months.push({ name: name.trim(), days });
|
|
79
|
+
}
|
|
80
|
+
cal.months = months;
|
|
81
|
+
}
|
|
82
|
+
return cal;
|
|
83
|
+
}
|
|
84
|
+
/** A script-file date token (`3000.1.1`), or null when the text is not one. */
|
|
85
|
+
function parseScriptDate(text) {
|
|
86
|
+
const match = /^(\d{1,5})\.(\d{1,2})\.(\d{1,2})$/.exec(text);
|
|
87
|
+
if (!match)
|
|
88
|
+
return null;
|
|
89
|
+
return { y: Number(match[1]), m: Number(match[2]), d: Number(match[3]) };
|
|
90
|
+
}
|
|
91
|
+
/** Month/day within the calendar's bounds (year just has to be positive). */
|
|
92
|
+
function isValidScriptDate(cal, y, m, d) {
|
|
93
|
+
const months = monthsOf(cal);
|
|
94
|
+
return y >= 1 && m >= 1 && m <= months.length && d >= 1 && d <= months[m - 1].days;
|
|
95
|
+
}
|
|
96
|
+
/** Era-mapped year: "1000 BC". Null for pre-epoch years of a single-era calendar. */
|
|
97
|
+
function displayYear(cal, y) {
|
|
98
|
+
if (y >= cal.epoch)
|
|
99
|
+
return `${y - cal.epoch + 1} ${cal.after}`;
|
|
100
|
+
return cal.before ? `${cal.epoch - y} ${cal.before}` : null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Full display form of a script date: "1000 BC" for the year's first day
|
|
104
|
+
* (how start dates read in game), "15 March 1000 BC" otherwise. Null when the
|
|
105
|
+
* date does not fit the calendar.
|
|
106
|
+
*/
|
|
107
|
+
function displayDate(cal, y, m, d) {
|
|
108
|
+
if (!isValidScriptDate(cal, y, m, d))
|
|
109
|
+
return null;
|
|
110
|
+
const year = displayYear(cal, y);
|
|
111
|
+
if (!year)
|
|
112
|
+
return null;
|
|
113
|
+
if (m === 1 && d === 1)
|
|
114
|
+
return year;
|
|
115
|
+
return `${d} ${monthsOf(cal)[m - 1].name} ${year}`;
|
|
116
|
+
}
|
|
117
|
+
/** Case-insensitive month lookup: exact name, else unique prefix. */
|
|
118
|
+
function monthByName(cal, text) {
|
|
119
|
+
const needle = text.toLowerCase();
|
|
120
|
+
const months = monthsOf(cal);
|
|
121
|
+
const exact = months.findIndex((m) => m.name.toLowerCase() === needle);
|
|
122
|
+
if (exact >= 0)
|
|
123
|
+
return exact + 1;
|
|
124
|
+
const prefixed = months
|
|
125
|
+
.map((m, i) => ({ m, i }))
|
|
126
|
+
.filter(({ m }) => m.name.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(cal, y, m, d)) {
|
|
177
|
+
const months = monthsOf(cal);
|
|
178
|
+
return m >= 1 && m <= months.length
|
|
179
|
+
? { ok: false, error: `${months[m - 1].name} has ${months[m - 1].days} days` }
|
|
180
|
+
: { ok: false, error: `this calendar has ${months.length} months` };
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, script: `${y}.${m}.${d}`, display: displayDate(cal, y, m, d) };
|
|
183
|
+
}
|
|
@@ -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,105 @@
|
|
|
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
|
+
const calendar_1 = require("./calendar");
|
|
6
|
+
/** Keys the generated files define; exported so tests and docs stay in sync. */
|
|
7
|
+
exports.CAL_YEAR_KEY = "PX_CAL_YEAR";
|
|
8
|
+
exports.CAL_ERA_KEY = "PX_CAL_ERA";
|
|
9
|
+
/** Era labels sit inside '...' CString literals; a quote would break the
|
|
10
|
+
* datafunction silently, which is exactly the failure class we exist to
|
|
11
|
+
* prevent. */
|
|
12
|
+
function cstring(label) {
|
|
13
|
+
return label.replace(/'/g, "");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `[Select_int32(...)]` era math for the year, mirroring
|
|
17
|
+
* calendar.ts `displayYear`: y >= epoch shows as y - (epoch - 1), below as
|
|
18
|
+
* epoch - y. A single-era calendar (no `before`) clamps to 1 instead, so
|
|
19
|
+
* pre-epoch years never display as 0 or negative.
|
|
20
|
+
*/
|
|
21
|
+
function yearExpr(cal) {
|
|
22
|
+
const year = "'(int32)$YEAR|q$'";
|
|
23
|
+
const toAfter = `Subtract_int32( ${year}, '(int32)${cal.epoch - 1}' )`;
|
|
24
|
+
if (!cal.before)
|
|
25
|
+
return `[Max_int32( ${toAfter}, '(int32)1' )]`;
|
|
26
|
+
const toBefore = `Subtract_int32( '(int32)${cal.epoch}', ${year} )`;
|
|
27
|
+
return `[Select_int32( GreaterThanOrEqualTo_int32( ${year}, '(int32)${cal.epoch}' ), ${toAfter}, ${toBefore} )]`;
|
|
28
|
+
}
|
|
29
|
+
function eraExpr(cal) {
|
|
30
|
+
if (!cal.before)
|
|
31
|
+
return cal.after;
|
|
32
|
+
return (`[Select_CString( GreaterThanOrEqualTo_int32( '(int32)$YEAR|q$', '(int32)${cal.epoch}' ), ` +
|
|
33
|
+
`'${cstring(cal.after)}', '${cstring(cal.before)}' )]`);
|
|
34
|
+
}
|
|
35
|
+
function locFile(lang, lines) {
|
|
36
|
+
return [`l_${lang}:`, ...lines, ""].join("\n");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The two files implementing the calendar in game:
|
|
40
|
+
*
|
|
41
|
+
* - `localization/<lang>/px_calendar_l_<lang>.yml` - the era-math keys
|
|
42
|
+
* (new keys, so NOT in replace/).
|
|
43
|
+
* - `localization/replace/<lang>/px_calendar_dates_l_<lang>.yml` - the
|
|
44
|
+
* vanilla date-format overrides routing `$YEAR$` through them, plus the
|
|
45
|
+
* engine month-name overrides when the calendar has custom months.
|
|
46
|
+
*
|
|
47
|
+
* Deterministic filenames: regenerating after a `px.calendar` change
|
|
48
|
+
* overwrites the same two files.
|
|
49
|
+
*/
|
|
50
|
+
function generateCalendarLoc(cal, spec, lang) {
|
|
51
|
+
const notes = [];
|
|
52
|
+
const header = [
|
|
53
|
+
" # Generated by 'Paradox: Generate Calendar Localization' from the px.calendar",
|
|
54
|
+
" # setting. Edit px.calendar and regenerate instead of editing by hand.",
|
|
55
|
+
];
|
|
56
|
+
const eraDoc = cal.before
|
|
57
|
+
? ` # Script year >= ${cal.epoch} shows as (year - ${cal.epoch - 1}) ${cal.after}; below as (${cal.epoch} - year) ${cal.before}.`
|
|
58
|
+
: ` # Script year >= ${cal.epoch} shows as (year - ${cal.epoch - 1}) ${cal.after}; earlier years clamp to 1.`;
|
|
59
|
+
const mathLines = [
|
|
60
|
+
...header,
|
|
61
|
+
eraDoc,
|
|
62
|
+
` ${exports.CAL_YEAR_KEY}:0 "${yearExpr(cal)}"`,
|
|
63
|
+
` ${exports.CAL_ERA_KEY}:0 "${eraExpr(cal)}"`,
|
|
64
|
+
];
|
|
65
|
+
const overrideLines = [
|
|
66
|
+
...header,
|
|
67
|
+
" # Overrides of the engine's date-format keys: same formats as vanilla, with",
|
|
68
|
+
` # the raw $YEAR$ routed through ${exports.CAL_YEAR_KEY}/${exports.CAL_ERA_KEY}.`,
|
|
69
|
+
];
|
|
70
|
+
for (const [key, format] of Object.entries(spec.dateFormats)) {
|
|
71
|
+
const value = format.replace("{year}", `$${exports.CAL_YEAR_KEY}$`).replace("{era}", `$${exports.CAL_ERA_KEY}$`);
|
|
72
|
+
overrideLines.push(` ${key}:0 "${value}"`);
|
|
73
|
+
}
|
|
74
|
+
if (cal.months) {
|
|
75
|
+
const months = (0, calendar_1.monthsOf)(cal);
|
|
76
|
+
if (!spec.monthKeys) {
|
|
77
|
+
notes.push("This game's month-name keys are not mapped yet; custom month names were not generated.");
|
|
78
|
+
}
|
|
79
|
+
else if (months.length !== spec.monthKeys.length) {
|
|
80
|
+
notes.push(`The engine has exactly ${spec.monthKeys.length} months; your calendar declares ${months.length}, ` +
|
|
81
|
+
"so month names were not generated (the month count itself cannot be modded).");
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
overrideLines.push(" # Engine month names (long and abbreviated forms both get the custom name).");
|
|
85
|
+
months.forEach((m, i) => {
|
|
86
|
+
const [long, short] = spec.monthKeys[i];
|
|
87
|
+
overrideLines.push(` ${long}:0 "${m.name.replace(/"/g, '\\"')}"`);
|
|
88
|
+
if (short !== long)
|
|
89
|
+
overrideLines.push(` ${short}:0 "${m.name.replace(/"/g, '\\"')}"`);
|
|
90
|
+
});
|
|
91
|
+
notes.push("Month day counts are engine-fixed (31/28/31...); custom day counts only affect the editor's display.");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
notes.push(`Generated for '${lang}' only; other languages the mod ships need the same overrides in their replace folder.`);
|
|
95
|
+
return {
|
|
96
|
+
files: [
|
|
97
|
+
{ relPath: `localization/${lang}/px_calendar_l_${lang}.yml`, content: locFile(lang, mathLines) },
|
|
98
|
+
{
|
|
99
|
+
relPath: `localization/replace/${lang}/px_calendar_dates_l_${lang}.yml`,
|
|
100
|
+
content: locFile(lang, overrideLines),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
notes,
|
|
104
|
+
};
|
|
105
|
+
}
|
package/dist/descriptorMod.d.ts
CHANGED
|
@@ -53,6 +53,11 @@ export declare function validateDescriptor(text: string, opts: {
|
|
|
53
53
|
* something comes from ("Community Flavor Pack") instead of a generic "mod".
|
|
54
54
|
*/
|
|
55
55
|
export declare function readDescriptorName(dir: string): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* The quoted strings inside a top-level `<key>={ "A" "B" }` block of a .mod
|
|
58
|
+
* text, in file order; empty when the block is missing.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readDescriptorBlock(text: string, key: string): string[];
|
|
56
61
|
/**
|
|
57
62
|
* The mod names inside `<dir>/descriptor.mod`'s `dependencies={ "A" "B" }`
|
|
58
63
|
* block, in file order; empty when the file or the block is missing. The
|
|
@@ -60,6 +65,22 @@ export declare function readDescriptorName(dir: string): string | null;
|
|
|
60
65
|
* Workshop id, so that is what the caller compares them with.
|
|
61
66
|
*/
|
|
62
67
|
export declare function readDescriptorDependencies(dir: string): string[];
|
|
68
|
+
/**
|
|
69
|
+
* `text` with the top-level `key="value"` entry replaced, or appended when the
|
|
70
|
+
* key is absent. Only scalar entries: a key whose value is a block is left
|
|
71
|
+
* alone and the entry is appended instead. Line endings and a leading BOM
|
|
72
|
+
* survive untouched; the appended line follows the file's dominant EOL.
|
|
73
|
+
* The value is made descriptor-safe like upsertDescriptorBlock's quoting:
|
|
74
|
+
* the format has no escape, so double quotes become apostrophes and line
|
|
75
|
+
* breaks collapse to one space.
|
|
76
|
+
*/
|
|
77
|
+
export declare function upsertDescriptorValue(text: string, key: string, value: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* `text` with the top-level `key={...}` block replaced by one holding exactly
|
|
80
|
+
* `values` (quoted, tab-indented, the file's EOL), or appended when absent.
|
|
81
|
+
* Descriptor blocks are flat, so the block ends at the first `}`-only line.
|
|
82
|
+
*/
|
|
83
|
+
export declare function upsertDescriptorBlock(text: string, key: string, values: string[]): string;
|
|
63
84
|
/** "1.19.0.6" -> "1.19.*" (the wildcard form that survives hotfixes). */
|
|
64
85
|
export declare function wildcardVersion(raw: string): string | null;
|
|
65
86
|
/** A launcher-correct starter descriptor.mod. */
|
package/dist/descriptorMod.js
CHANGED
|
@@ -37,7 +37,10 @@ exports.LAUNCHER_TAGS = exports.DESCRIPTOR_FIELD_MAP = exports.DESCRIPTOR_FIELDS
|
|
|
37
37
|
exports.parseDescriptor = parseDescriptor;
|
|
38
38
|
exports.validateDescriptor = validateDescriptor;
|
|
39
39
|
exports.readDescriptorName = readDescriptorName;
|
|
40
|
+
exports.readDescriptorBlock = readDescriptorBlock;
|
|
40
41
|
exports.readDescriptorDependencies = readDescriptorDependencies;
|
|
42
|
+
exports.upsertDescriptorValue = upsertDescriptorValue;
|
|
43
|
+
exports.upsertDescriptorBlock = upsertDescriptorBlock;
|
|
41
44
|
exports.wildcardVersion = wildcardVersion;
|
|
42
45
|
exports.scaffoldDescriptor = scaffoldDescriptor;
|
|
43
46
|
/**
|
|
@@ -296,6 +299,17 @@ function readDescriptorName(dir) {
|
|
|
296
299
|
const value = entry.value.replace(/^"([^]*)"$/, "$1").trim();
|
|
297
300
|
return value === "" ? null : value;
|
|
298
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* The quoted strings inside a top-level `<key>={ "A" "B" }` block of a .mod
|
|
304
|
+
* text, in file order; empty when the block is missing.
|
|
305
|
+
*/
|
|
306
|
+
function readDescriptorBlock(text, key) {
|
|
307
|
+
// Comments first: a commented-out entry is not an entry.
|
|
308
|
+
const block = new RegExp(`(?:^|\\n)[ \\t]*${key}[ \\t]*=[ \\t]*\\{([^}]*)\\}`).exec(text.replace(/#[^\n]*/g, ""));
|
|
309
|
+
if (!block)
|
|
310
|
+
return [];
|
|
311
|
+
return [...block[1].matchAll(/"([^"]*)"/g)].map((m) => m[1].trim()).filter((s) => s !== "");
|
|
312
|
+
}
|
|
299
313
|
/**
|
|
300
314
|
* The mod names inside `<dir>/descriptor.mod`'s `dependencies={ "A" "B" }`
|
|
301
315
|
* block, in file order; empty when the file or the block is missing. The
|
|
@@ -310,11 +324,53 @@ function readDescriptorDependencies(dir) {
|
|
|
310
324
|
catch {
|
|
311
325
|
return [];
|
|
312
326
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
327
|
+
return readDescriptorBlock(text, "dependencies");
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* `text` with the top-level `key="value"` entry replaced, or appended when the
|
|
331
|
+
* key is absent. Only scalar entries: a key whose value is a block is left
|
|
332
|
+
* alone and the entry is appended instead. Line endings and a leading BOM
|
|
333
|
+
* survive untouched; the appended line follows the file's dominant EOL.
|
|
334
|
+
* The value is made descriptor-safe like upsertDescriptorBlock's quoting:
|
|
335
|
+
* the format has no escape, so double quotes become apostrophes and line
|
|
336
|
+
* breaks collapse to one space.
|
|
337
|
+
*/
|
|
338
|
+
function upsertDescriptorValue(text, key, value) {
|
|
339
|
+
const v = value.replace(/"/g, "'").replace(/\s*\r?\n\s*/g, " ");
|
|
340
|
+
const entry = parseDescriptor(text).find((e) => e.key === key && e.value !== "");
|
|
341
|
+
if (entry) {
|
|
342
|
+
const lines = text.split(/(\r?\n)/); // keep separators at odd indices
|
|
343
|
+
const idx = entry.line * 2;
|
|
344
|
+
lines[idx] = lines[idx].replace(/=\s*("[^"]*"|\S+)([ \t]*(#.*)?)$/, (_m, _old, tail) => `="${v}"${tail}`);
|
|
345
|
+
return lines.join("");
|
|
346
|
+
}
|
|
347
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
348
|
+
const sep = text === "" || text.endsWith("\n") ? "" : eol;
|
|
349
|
+
return `${text}${sep}${key}="${v}"${eol}`;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* `text` with the top-level `key={...}` block replaced by one holding exactly
|
|
353
|
+
* `values` (quoted, tab-indented, the file's EOL), or appended when absent.
|
|
354
|
+
* Descriptor blocks are flat, so the block ends at the first `}`-only line.
|
|
355
|
+
*/
|
|
356
|
+
function upsertDescriptorBlock(text, key, values) {
|
|
357
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
358
|
+
const q = (v) => `"${v.replace(/"/g, "'")}"`;
|
|
359
|
+
const block = [`${key}={`, ...values.map((v) => `\t${q(v)}`), `}`].join(eol);
|
|
360
|
+
const lines = text.split(/\r?\n/);
|
|
361
|
+
const open = lines.findIndex((l) => new RegExp(`^\\s*${key}\\s*=\\s*\\{`).test(l));
|
|
362
|
+
if (open >= 0) {
|
|
363
|
+
let close = open;
|
|
364
|
+
// A one-line block (`tags={ "x" }`) closes on its own line.
|
|
365
|
+
if (!/\}\s*(#.*)?$/.test(lines[open])) {
|
|
366
|
+
while (close < lines.length - 1 && !/^\s*\}\s*(#.*)?$/.test(lines[close]))
|
|
367
|
+
close++;
|
|
368
|
+
}
|
|
369
|
+
lines.splice(open, close - open + 1, block);
|
|
370
|
+
return lines.join(eol);
|
|
371
|
+
}
|
|
372
|
+
const sep = text === "" || text.endsWith("\n") ? "" : eol;
|
|
373
|
+
return `${text}${sep}${block}${eol}`;
|
|
318
374
|
}
|
|
319
375
|
/** "1.19.0.6" -> "1.19.*" (the wildcard form that survives hotfixes). */
|
|
320
376
|
function wildcardVersion(raw) {
|
package/dist/kinds.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One kind map, four surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Every place the product names a concept - the hover badge, the completion
|
|
5
|
+
* list icon, the tree leaf, the breadcrumb/outline entry - reads its glyph from
|
|
6
|
+
* here, so a trigger looks like a trigger everywhere.
|
|
7
|
+
*
|
|
8
|
+
* The colour is not a second decision. VS Code paints a completion row from the
|
|
9
|
+
* `symbolIcon.*Foreground` token of the `CompletionItemKind` we send, and we
|
|
10
|
+
* cannot override it, so the kind IS the colour. The hover badge reuses that
|
|
11
|
+
* same token, which is why there is no colour column to keep in sync. Four
|
|
12
|
+
* groups come out of that, and choosing the kind is choosing the group:
|
|
13
|
+
*
|
|
14
|
+
* purple asks a question Method
|
|
15
|
+
* orange makes it happen Class, Event, Enum, Value
|
|
16
|
+
* blue you stored it Variable, Field, Interface, EnumMember
|
|
17
|
+
* grey syntax, everything else all the rest
|
|
18
|
+
*
|
|
19
|
+
* Three facts shape the table and are easy to re-break:
|
|
20
|
+
*
|
|
21
|
+
* 1. **Codicon aliases collapse.** `symbol-method`, `symbol-function` and
|
|
22
|
+
* `symbol-constructor` are one codepoint, so they are one picture. Same for
|
|
23
|
+
* `symbol-enum`/`symbol-value`, `symbol-key`/`symbol-text`,
|
|
24
|
+
* `symbol-struct`/`symbol-structure`, `symbol-unit`/`symbol-ruler` and
|
|
25
|
+
* `symbol-type-parameter`/`symbol-parameter`. Check a proposed mapping
|
|
26
|
+
* against codepoints, not against the names. Prefer the canonical name of a
|
|
27
|
+
* pair: only it carries the `symbolIcon.*Foreground` rule, so a themed tree
|
|
28
|
+
* leaf tints and an alias does not.
|
|
29
|
+
* 2. **Only `CompletionItemKind` reaches the suggest widget.** 25 values,
|
|
30
|
+
* 22 distinct pictures after the collapse. A concept that appears in a
|
|
31
|
+
* completion list cannot use a glyph from outside that set in the list,
|
|
32
|
+
* even though the hover and the tree can draw all 461 codicons.
|
|
33
|
+
* 3. **Only `SymbolKind` reaches the outline.** Breadcrumbs, the outline,
|
|
34
|
+
* sticky scroll and Ctrl+T take an LSP `SymbolKind`, and VS Code draws
|
|
35
|
+
* member `X` with the codicon `symbol-<kebab X>`. `symbolKind` names the
|
|
36
|
+
* member drawing the same picture as `codicon`, so the breadcrumb bar and
|
|
37
|
+
* the hover badge cannot disagree; it is null for a picture no member
|
|
38
|
+
* draws. The server resolves the name to the numeric enum.
|
|
39
|
+
*
|
|
40
|
+
* `codicon`, `completionKind` and `symbolKind` are separate fields for exactly
|
|
41
|
+
* that reason. They name the same picture everywhere except two entries:
|
|
42
|
+
* `texture`, whose `file-media` glyph no completion kind can produce, and
|
|
43
|
+
* `list`, whose array glyph no *free* completion kind can produce (the colour
|
|
44
|
+
* has to stay blue, and all four blue kinds are taken), so the suggest widget
|
|
45
|
+
* alone still draws it as an enum member.
|
|
46
|
+
*
|
|
47
|
+
* Uniqueness is promised *within a completion list*, not globally: script, gui
|
|
48
|
+
* and datafunction completions never appear together, so they may share glyphs.
|
|
49
|
+
*
|
|
50
|
+
* No imports: this is shared by the server and the VS Code client.
|
|
51
|
+
*/
|
|
52
|
+
export interface KindStyle {
|
|
53
|
+
/** Codicon id, drawn in the hover badge and the tree. */
|
|
54
|
+
codicon: string;
|
|
55
|
+
/** `CompletionItemKind` member name; the server maps it to the enum. */
|
|
56
|
+
completionKind: string;
|
|
57
|
+
/**
|
|
58
|
+
* `SymbolKind` member name drawing the same picture as `codicon`, or null
|
|
59
|
+
* when no member draws it. The server maps it to the enum.
|
|
60
|
+
*/
|
|
61
|
+
symbolKind: string | null;
|
|
62
|
+
/** Hover badge colour as a `--vscode-symbolIcon-*` var, or null for none. */
|
|
63
|
+
color: string | null;
|
|
64
|
+
}
|
|
65
|
+
/** Anything the map does not name: a definition we have no opinion about. */
|
|
66
|
+
export declare const DEFAULT_KIND_STYLE: KindStyle;
|
|
67
|
+
/** Glyph, completion kind and badge colour for a kind name. Never throws. */
|
|
68
|
+
export declare function kindStyle(kind: string): KindStyle;
|
|
69
|
+
/** True when the map has an opinion, i.e. the kind is not falling through. */
|
|
70
|
+
export declare function hasKindStyle(kind: string): boolean;
|
|
71
|
+
/** Every mapped kind, for the coverage test that keeps this table honest. */
|
|
72
|
+
export declare function mappedKinds(): string[];
|