@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
|
@@ -0,0 +1,151 @@
|
|
|
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.STEAM_LANGUAGES = void 0;
|
|
37
|
+
exports.workshopMetaRelPath = workshopMetaRelPath;
|
|
38
|
+
exports.readWorkshopMeta = readWorkshopMeta;
|
|
39
|
+
exports.upsertWorkshopMeta = upsertWorkshopMeta;
|
|
40
|
+
exports.steamLanguageLabel = steamLanguageLabel;
|
|
41
|
+
exports.steamLanguageForLoc = steamLanguageForLoc;
|
|
42
|
+
/**
|
|
43
|
+
* The mod's local Workshop record: `<mod>/<configDir>/workshop.json`. It holds
|
|
44
|
+
* what the game's own descriptor has no field for - the item's description and
|
|
45
|
+
* its per-language translations (title + description shown to Workshop
|
|
46
|
+
* visitors browsing Steam in that language), plus the published id for the
|
|
47
|
+
* `.metadata` games (launcher-`.mod` games keep the id in `remote_file_id`).
|
|
48
|
+
*
|
|
49
|
+
* The local file is the canonical copy once the user manages the item from the
|
|
50
|
+
* toolkit: uploads read from here, and "fetch" pulls the live values down into
|
|
51
|
+
* it. Reads and writes are merge-preserving: keys this version does not know
|
|
52
|
+
* survive a round trip.
|
|
53
|
+
*
|
|
54
|
+
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
55
|
+
*/
|
|
56
|
+
const fs = __importStar(require("fs"));
|
|
57
|
+
const path = __importStar(require("path"));
|
|
58
|
+
/** Mod-root-relative path of the record, forward slashes. */
|
|
59
|
+
function workshopMetaRelPath(configDirName) {
|
|
60
|
+
return `${configDirName}/workshop.json`;
|
|
61
|
+
}
|
|
62
|
+
/** The parsed `<dir>/<configDir>/workshop.json`, or null when absent/unreadable. */
|
|
63
|
+
function readWorkshopMeta(dir, configDirName) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, configDirName, "workshop.json"), "utf8"));
|
|
66
|
+
if (typeof raw !== "object" || raw === null)
|
|
67
|
+
return null;
|
|
68
|
+
return raw;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Merge `patch` into the record and write it back. Unknown keys of the file
|
|
76
|
+
* survive; a patch key set to `undefined` is left as it was. `translations`
|
|
77
|
+
* replaces as a whole (the caller edits the full map).
|
|
78
|
+
*/
|
|
79
|
+
function upsertWorkshopMeta(dir, configDirName, patch) {
|
|
80
|
+
const file = path.join(dir, configDirName, "workshop.json");
|
|
81
|
+
const current = (readWorkshopMeta(dir, configDirName) ?? {});
|
|
82
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
83
|
+
if (value !== undefined)
|
|
84
|
+
current[key] = value;
|
|
85
|
+
}
|
|
86
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
87
|
+
fs.writeFileSync(file, JSON.stringify(current, null, 2) + "\n", "utf8");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The languages the Workshop accepts item text in: Steam's API language codes
|
|
91
|
+
* with their English names, in Steam's documented order.
|
|
92
|
+
* https://partner.steamgames.com/doc/store/localization/languages
|
|
93
|
+
*/
|
|
94
|
+
exports.STEAM_LANGUAGES = [
|
|
95
|
+
{ api: "arabic", label: "Arabic" },
|
|
96
|
+
{ api: "bulgarian", label: "Bulgarian" },
|
|
97
|
+
{ api: "schinese", label: "Chinese (Simplified)" },
|
|
98
|
+
{ api: "tchinese", label: "Chinese (Traditional)" },
|
|
99
|
+
{ api: "czech", label: "Czech" },
|
|
100
|
+
{ api: "danish", label: "Danish" },
|
|
101
|
+
{ api: "dutch", label: "Dutch" },
|
|
102
|
+
{ api: "english", label: "English" },
|
|
103
|
+
{ api: "finnish", label: "Finnish" },
|
|
104
|
+
{ api: "french", label: "French" },
|
|
105
|
+
{ api: "german", label: "German" },
|
|
106
|
+
{ api: "greek", label: "Greek" },
|
|
107
|
+
{ api: "hungarian", label: "Hungarian" },
|
|
108
|
+
{ api: "indonesian", label: "Indonesian" },
|
|
109
|
+
{ api: "italian", label: "Italian" },
|
|
110
|
+
{ api: "japanese", label: "Japanese" },
|
|
111
|
+
{ api: "koreana", label: "Korean" },
|
|
112
|
+
{ api: "norwegian", label: "Norwegian" },
|
|
113
|
+
{ api: "polish", label: "Polish" },
|
|
114
|
+
{ api: "portuguese", label: "Portuguese" },
|
|
115
|
+
{ api: "brazilian", label: "Portuguese (Brazil)" },
|
|
116
|
+
{ api: "romanian", label: "Romanian" },
|
|
117
|
+
{ api: "russian", label: "Russian" },
|
|
118
|
+
{ api: "spanish", label: "Spanish (Spain)" },
|
|
119
|
+
{ api: "latam", label: "Spanish (Latin America)" },
|
|
120
|
+
{ api: "swedish", label: "Swedish" },
|
|
121
|
+
{ api: "thai", label: "Thai" },
|
|
122
|
+
{ api: "turkish", label: "Turkish" },
|
|
123
|
+
{ api: "ukrainian", label: "Ukrainian" },
|
|
124
|
+
{ api: "vietnamese", label: "Vietnamese" },
|
|
125
|
+
];
|
|
126
|
+
/** English name of a Steam API language code; the code itself when unknown. */
|
|
127
|
+
function steamLanguageLabel(api) {
|
|
128
|
+
return exports.STEAM_LANGUAGES.find((l) => l.api === api)?.label ?? api;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Steam API language code for a Paradox localization folder language
|
|
132
|
+
* (`translationCore.ts` LOC_LANGUAGES), or null when Steam has no counterpart.
|
|
133
|
+
* The two vocabularies differ where Steam's codes predate its own store pages
|
|
134
|
+
* (`koreana`, `schinese`).
|
|
135
|
+
*/
|
|
136
|
+
function steamLanguageForLoc(locLanguage) {
|
|
137
|
+
const map = {
|
|
138
|
+
english: "english",
|
|
139
|
+
french: "french",
|
|
140
|
+
german: "german",
|
|
141
|
+
spanish: "spanish",
|
|
142
|
+
russian: "russian",
|
|
143
|
+
korean: "koreana",
|
|
144
|
+
simp_chinese: "schinese",
|
|
145
|
+
japanese: "japanese",
|
|
146
|
+
polish: "polish",
|
|
147
|
+
braz_por: "brazilian",
|
|
148
|
+
turkish: "turkish",
|
|
149
|
+
};
|
|
150
|
+
return map[locLanguage] ?? null;
|
|
151
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@px-lsp/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "GPL-3.0-or-later",
|
|
5
5
|
"description": "Wire contract (custom LSP requests/notifications, settings types) and shared helpers for the px-lsp language server and its clients.",
|
|
6
6
|
"keywords": [
|
package/src/arrays.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
/** Array helpers shared by the server and the client. */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Append every element of `source` to `target`.
|
|
5
|
-
*
|
|
6
|
-
* `target.push(...source)` passes one argument per element and throws
|
|
7
|
-
* `RangeError: Maximum call stack size exceeded` past ~125k elements (measured,
|
|
8
|
-
* node 24, default stack). On the index paths that is a size-triggered crash:
|
|
9
|
-
* one engine/vanilla root already carries ~460k definitions and one generated
|
|
10
|
-
* mod file can carry six figures on its own. The loop has no ceiling and
|
|
11
|
-
* measures the same as the spread (2M elements appended in 10k pieces: 23 ms
|
|
12
|
-
* loop vs 22 ms spread; as one 500k piece: 5.5 ms loop vs 9.6 ms spread).
|
|
13
|
-
*/
|
|
14
|
-
export function pushAll<T>(target: T[], source: readonly T[]): void {
|
|
15
|
-
for (let i = 0; i < source.length; i++) target.push(source[i]);
|
|
16
|
-
}
|
|
1
|
+
/** Array helpers shared by the server and the client. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Append every element of `source` to `target`.
|
|
5
|
+
*
|
|
6
|
+
* `target.push(...source)` passes one argument per element and throws
|
|
7
|
+
* `RangeError: Maximum call stack size exceeded` past ~125k elements (measured,
|
|
8
|
+
* node 24, default stack). On the index paths that is a size-triggered crash:
|
|
9
|
+
* one engine/vanilla root already carries ~460k definitions and one generated
|
|
10
|
+
* mod file can carry six figures on its own. The loop has no ceiling and
|
|
11
|
+
* measures the same as the spread (2M elements appended in 10k pieces: 23 ms
|
|
12
|
+
* loop vs 22 ms spread; as one 500k piece: 5.5 ms loop vs 9.6 ms spread).
|
|
13
|
+
*/
|
|
14
|
+
export function pushAll<T>(target: T[], source: readonly T[]): void {
|
|
15
|
+
for (let i = 0; i < source.length; i++) target.push(source[i]);
|
|
16
|
+
}
|
package/src/calendar.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
|
|
14
|
+
export interface CalendarMonth {
|
|
15
|
+
/** Display name ("March", "Narvinye"). */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Day count; the engine has no leap years, so one number per month. */
|
|
18
|
+
days: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CalendarSetting {
|
|
22
|
+
/** Script year displayed as year 1 of the `after` era (no year zero). */
|
|
23
|
+
epoch: number;
|
|
24
|
+
/** Era label for script years >= epoch ("AD", "TA"...). */
|
|
25
|
+
after: string;
|
|
26
|
+
/** Era label for script years < epoch ("BC"). Omitted = single-era
|
|
27
|
+
* calendar: years before the epoch get no display form. */
|
|
28
|
+
before?: string;
|
|
29
|
+
/** Custom month names and day counts, first month first. Omitted = the
|
|
30
|
+
* standard 12 months (Feb 28: the engine has no leap years). */
|
|
31
|
+
months?: CalendarMonth[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
35
|
+
export const GREGORIAN_MONTHS: CalendarMonth[] = [
|
|
36
|
+
"January",
|
|
37
|
+
"February",
|
|
38
|
+
"March",
|
|
39
|
+
"April",
|
|
40
|
+
"May",
|
|
41
|
+
"June",
|
|
42
|
+
"July",
|
|
43
|
+
"August",
|
|
44
|
+
"September",
|
|
45
|
+
"October",
|
|
46
|
+
"November",
|
|
47
|
+
"December",
|
|
48
|
+
].map((name, i) => ({ name, days: DAYS[i] }));
|
|
49
|
+
|
|
50
|
+
export function monthsOf(cal: CalendarSetting): CalendarMonth[] {
|
|
51
|
+
return cal.months ?? GREGORIAN_MONTHS;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate a calendar straight out of JSON settings (any client, any hand-
|
|
56
|
+
* edited settings file). Returns a clean copy, or undefined when the value is
|
|
57
|
+
* not a usable calendar - features then behave as if none was configured.
|
|
58
|
+
*/
|
|
59
|
+
export function sanitizeCalendar(raw: unknown): CalendarSetting | undefined {
|
|
60
|
+
if (typeof raw !== "object" || raw === null) return undefined;
|
|
61
|
+
const o = raw as Record<string, unknown>;
|
|
62
|
+
const epoch = o.epoch;
|
|
63
|
+
const after = o.after;
|
|
64
|
+
if (typeof epoch !== "number" || !Number.isInteger(epoch) || epoch < 1) return undefined;
|
|
65
|
+
if (typeof after !== "string" || after.trim() === "") return undefined;
|
|
66
|
+
const cal: CalendarSetting = { epoch, after: after.trim() };
|
|
67
|
+
if (typeof o.before === "string" && o.before.trim() !== "") cal.before = o.before.trim();
|
|
68
|
+
// Typed input picks the era by its label and the month by its name, so a
|
|
69
|
+
// collision would resolve silently to the wrong one: not a usable calendar.
|
|
70
|
+
if (cal.before && cal.before.toLowerCase() === cal.after.toLowerCase()) return undefined;
|
|
71
|
+
if (Array.isArray(o.months) && o.months.length > 0) {
|
|
72
|
+
const months: CalendarMonth[] = [];
|
|
73
|
+
const seen = new Set<string>();
|
|
74
|
+
for (const m of o.months) {
|
|
75
|
+
if (typeof m !== "object" || m === null) return undefined;
|
|
76
|
+
const { name, days } = m as Record<string, unknown>;
|
|
77
|
+
if (typeof name !== "string" || name.trim() === "") return undefined;
|
|
78
|
+
if (typeof days !== "number" || !Number.isInteger(days) || days < 1 || days > 999) return undefined;
|
|
79
|
+
const key = name.trim().toLowerCase();
|
|
80
|
+
if (seen.has(key)) return undefined;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
months.push({ name: name.trim(), days });
|
|
83
|
+
}
|
|
84
|
+
cal.months = months;
|
|
85
|
+
}
|
|
86
|
+
return cal;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A script-file date token (`3000.1.1`), or null when the text is not one. */
|
|
90
|
+
export function parseScriptDate(text: string): { y: number; m: number; d: number } | null {
|
|
91
|
+
const match = /^(\d{1,5})\.(\d{1,2})\.(\d{1,2})$/.exec(text);
|
|
92
|
+
if (!match) return null;
|
|
93
|
+
return { y: Number(match[1]), m: Number(match[2]), d: Number(match[3]) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Month/day within the calendar's bounds (year just has to be positive). */
|
|
97
|
+
export function isValidScriptDate(cal: CalendarSetting, y: number, m: number, d: number): boolean {
|
|
98
|
+
const months = monthsOf(cal);
|
|
99
|
+
return y >= 1 && m >= 1 && m <= months.length && d >= 1 && d <= months[m - 1].days;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Era-mapped year: "1000 BC". Null for pre-epoch years of a single-era calendar. */
|
|
103
|
+
export function displayYear(cal: CalendarSetting, y: number): string | null {
|
|
104
|
+
if (y >= cal.epoch) return `${y - cal.epoch + 1} ${cal.after}`;
|
|
105
|
+
return cal.before ? `${cal.epoch - y} ${cal.before}` : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Full display form of a script date: "1000 BC" for the year's first day
|
|
110
|
+
* (how start dates read in game), "15 March 1000 BC" otherwise. Null when the
|
|
111
|
+
* date does not fit the calendar.
|
|
112
|
+
*/
|
|
113
|
+
export function displayDate(cal: CalendarSetting, y: number, m: number, d: number): string | null {
|
|
114
|
+
if (!isValidScriptDate(cal, y, m, d)) return null;
|
|
115
|
+
const year = displayYear(cal, y);
|
|
116
|
+
if (!year) return null;
|
|
117
|
+
if (m === 1 && d === 1) return year;
|
|
118
|
+
return `${d} ${monthsOf(cal)[m - 1].name} ${year}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ConvertResult = { ok: true; script: string; display: string } | { ok: false; error: string };
|
|
122
|
+
|
|
123
|
+
/** Case-insensitive month lookup: exact name, else unique prefix. */
|
|
124
|
+
function monthByName(cal: CalendarSetting, text: string): number | null {
|
|
125
|
+
const needle = text.toLowerCase();
|
|
126
|
+
const months = monthsOf(cal);
|
|
127
|
+
const exact = months.findIndex((m) => m.name.toLowerCase() === needle);
|
|
128
|
+
if (exact >= 0) return exact + 1;
|
|
129
|
+
const prefixed = months
|
|
130
|
+
.map((m, i) => ({ m, i }))
|
|
131
|
+
.filter(({ m }) => m.name.toLowerCase().startsWith(needle));
|
|
132
|
+
return prefixed.length === 1 ? prefixed[0].i + 1 : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A display-calendar date typed by the user -> the script date the file needs.
|
|
137
|
+
* Grammar: `YEAR [ERA] [MONTH [DAY]]`, e.g. "1000 BC", "1000 BC March 15",
|
|
138
|
+
* "1000 BC 3 15", "1 AD", "500" (era defaults to `after`). MONTH is a number
|
|
139
|
+
* or a month name (unique prefix is enough).
|
|
140
|
+
*/
|
|
141
|
+
export function convertDisplayInput(cal: CalendarSetting, input: string): ConvertResult {
|
|
142
|
+
const words = input.trim().split(/\s+/).filter(Boolean);
|
|
143
|
+
if (words.length === 0) return { ok: false, error: "type a year, e.g. 1000 " + (cal.before ?? cal.after) };
|
|
144
|
+
if (!/^\d{1,5}$/.test(words[0])) return { ok: false, error: `"${words[0]}" is not a year` };
|
|
145
|
+
const year = Number(words[0]);
|
|
146
|
+
let rest = words.slice(1);
|
|
147
|
+
|
|
148
|
+
let era = cal.after;
|
|
149
|
+
if (rest.length > 0) {
|
|
150
|
+
const w = rest[0].toLowerCase();
|
|
151
|
+
if (w === cal.after.toLowerCase()) {
|
|
152
|
+
rest = rest.slice(1);
|
|
153
|
+
} else if (cal.before && w === cal.before.toLowerCase()) {
|
|
154
|
+
era = cal.before;
|
|
155
|
+
rest = rest.slice(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let m = 1;
|
|
160
|
+
let d = 1;
|
|
161
|
+
if (rest.length > 0) {
|
|
162
|
+
const month = /^\d{1,2}$/.test(rest[0]) ? Number(rest[0]) : monthByName(cal, rest[0]);
|
|
163
|
+
if (month === null) return { ok: false, error: `"${rest[0]}" is not a month of this calendar` };
|
|
164
|
+
m = month;
|
|
165
|
+
rest = rest.slice(1);
|
|
166
|
+
}
|
|
167
|
+
if (rest.length > 0) {
|
|
168
|
+
if (!/^\d{1,3}$/.test(rest[0])) return { ok: false, error: `"${rest[0]}" is not a day` };
|
|
169
|
+
d = Number(rest[0]);
|
|
170
|
+
rest = rest.slice(1);
|
|
171
|
+
}
|
|
172
|
+
if (rest.length > 0) return { ok: false, error: `unexpected "${rest.join(" ")}"` };
|
|
173
|
+
|
|
174
|
+
if (year < 1) return { ok: false, error: "years start at 1 (no year zero)" };
|
|
175
|
+
const y = era === cal.after ? year + cal.epoch - 1 : cal.epoch - year;
|
|
176
|
+
if (y < 1) return { ok: false, error: `${year} ${era} is before script year 1 (epoch ${cal.epoch})` };
|
|
177
|
+
if (!isValidScriptDate(cal, y, m, d)) {
|
|
178
|
+
const months = monthsOf(cal);
|
|
179
|
+
return m >= 1 && m <= months.length
|
|
180
|
+
? { ok: false, error: `${months[m - 1].name} has ${months[m - 1].days} days` }
|
|
181
|
+
: { ok: false, error: `this calendar has ${months.length} months` };
|
|
182
|
+
}
|
|
183
|
+
return { ok: true, script: `${y}.${m}.${d}`, display: displayDate(cal, y, m, d)! };
|
|
184
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
import { monthsOf } from "./calendar";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The loc keys a game formats dates through, verified against the game's
|
|
17
|
+
* binary/files. Lives in the GameProfile (`games/<id>/meta.ts`); a profile
|
|
18
|
+
* without one does not support generation.
|
|
19
|
+
*/
|
|
20
|
+
export interface CalendarLocSpec {
|
|
21
|
+
/**
|
|
22
|
+
* Vanilla date-format keys to override in `localization/replace/`, mapped
|
|
23
|
+
* to their format template. `{year}` and `{era}` mark where the generated
|
|
24
|
+
* era-math key references go; everything else is kept verbatim (the
|
|
25
|
+
* vanilla `$DAY$`/`$MONTH$` parameters).
|
|
26
|
+
*/
|
|
27
|
+
dateFormats: Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* Engine month-name loc keys, first month first: [longKey, shortKey].
|
|
30
|
+
* Overridden only when the calendar declares custom months, and only when
|
|
31
|
+
* it declares exactly as many months as the engine has.
|
|
32
|
+
*/
|
|
33
|
+
monthKeys?: [string, string][];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One file the generator wants written, path relative to the mod root. */
|
|
37
|
+
export interface GeneratedLocFile {
|
|
38
|
+
/** Forward slashes; the writer joins it onto the mod root. */
|
|
39
|
+
relPath: string;
|
|
40
|
+
/** Content without BOM; the writer prepends it. */
|
|
41
|
+
content: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CalendarLocResult {
|
|
45
|
+
files: GeneratedLocFile[];
|
|
46
|
+
/** Facts the modder should know after generating (limits, follow-ups). */
|
|
47
|
+
notes: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Keys the generated files define; exported so tests and docs stay in sync. */
|
|
51
|
+
export const CAL_YEAR_KEY = "PX_CAL_YEAR";
|
|
52
|
+
export const CAL_ERA_KEY = "PX_CAL_ERA";
|
|
53
|
+
|
|
54
|
+
/** Era labels sit inside '...' CString literals; a quote would break the
|
|
55
|
+
* datafunction silently, which is exactly the failure class we exist to
|
|
56
|
+
* prevent. */
|
|
57
|
+
function cstring(label: string): string {
|
|
58
|
+
return label.replace(/'/g, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `[Select_int32(...)]` era math for the year, mirroring
|
|
63
|
+
* calendar.ts `displayYear`: y >= epoch shows as y - (epoch - 1), below as
|
|
64
|
+
* epoch - y. A single-era calendar (no `before`) clamps to 1 instead, so
|
|
65
|
+
* pre-epoch years never display as 0 or negative.
|
|
66
|
+
*/
|
|
67
|
+
function yearExpr(cal: CalendarSetting): string {
|
|
68
|
+
const year = "'(int32)$YEAR|q$'";
|
|
69
|
+
const toAfter = `Subtract_int32( ${year}, '(int32)${cal.epoch - 1}' )`;
|
|
70
|
+
if (!cal.before) return `[Max_int32( ${toAfter}, '(int32)1' )]`;
|
|
71
|
+
const toBefore = `Subtract_int32( '(int32)${cal.epoch}', ${year} )`;
|
|
72
|
+
return `[Select_int32( GreaterThanOrEqualTo_int32( ${year}, '(int32)${cal.epoch}' ), ${toAfter}, ${toBefore} )]`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function eraExpr(cal: CalendarSetting): string {
|
|
76
|
+
if (!cal.before) return cal.after;
|
|
77
|
+
return (
|
|
78
|
+
`[Select_CString( GreaterThanOrEqualTo_int32( '(int32)$YEAR|q$', '(int32)${cal.epoch}' ), ` +
|
|
79
|
+
`'${cstring(cal.after)}', '${cstring(cal.before)}' )]`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function locFile(lang: string, lines: string[]): string {
|
|
84
|
+
return [`l_${lang}:`, ...lines, ""].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The two files implementing the calendar in game:
|
|
89
|
+
*
|
|
90
|
+
* - `localization/<lang>/px_calendar_l_<lang>.yml` - the era-math keys
|
|
91
|
+
* (new keys, so NOT in replace/).
|
|
92
|
+
* - `localization/replace/<lang>/px_calendar_dates_l_<lang>.yml` - the
|
|
93
|
+
* vanilla date-format overrides routing `$YEAR$` through them, plus the
|
|
94
|
+
* engine month-name overrides when the calendar has custom months.
|
|
95
|
+
*
|
|
96
|
+
* Deterministic filenames: regenerating after a `px.calendar` change
|
|
97
|
+
* overwrites the same two files.
|
|
98
|
+
*/
|
|
99
|
+
export function generateCalendarLoc(
|
|
100
|
+
cal: CalendarSetting,
|
|
101
|
+
spec: CalendarLocSpec,
|
|
102
|
+
lang: string
|
|
103
|
+
): CalendarLocResult {
|
|
104
|
+
const notes: string[] = [];
|
|
105
|
+
const header = [
|
|
106
|
+
" # Generated by 'Paradox: Generate Calendar Localization' from the px.calendar",
|
|
107
|
+
" # setting. Edit px.calendar and regenerate instead of editing by hand.",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const eraDoc = cal.before
|
|
111
|
+
? ` # Script year >= ${cal.epoch} shows as (year - ${cal.epoch - 1}) ${cal.after}; below as (${cal.epoch} - year) ${cal.before}.`
|
|
112
|
+
: ` # Script year >= ${cal.epoch} shows as (year - ${cal.epoch - 1}) ${cal.after}; earlier years clamp to 1.`;
|
|
113
|
+
const mathLines = [
|
|
114
|
+
...header,
|
|
115
|
+
eraDoc,
|
|
116
|
+
` ${CAL_YEAR_KEY}:0 "${yearExpr(cal)}"`,
|
|
117
|
+
` ${CAL_ERA_KEY}:0 "${eraExpr(cal)}"`,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const overrideLines = [
|
|
121
|
+
...header,
|
|
122
|
+
" # Overrides of the engine's date-format keys: same formats as vanilla, with",
|
|
123
|
+
` # the raw $YEAR$ routed through ${CAL_YEAR_KEY}/${CAL_ERA_KEY}.`,
|
|
124
|
+
];
|
|
125
|
+
for (const [key, format] of Object.entries(spec.dateFormats)) {
|
|
126
|
+
const value = format.replace("{year}", `$${CAL_YEAR_KEY}$`).replace("{era}", `$${CAL_ERA_KEY}$`);
|
|
127
|
+
overrideLines.push(` ${key}:0 "${value}"`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (cal.months) {
|
|
131
|
+
const months = monthsOf(cal);
|
|
132
|
+
if (!spec.monthKeys) {
|
|
133
|
+
notes.push("This game's month-name keys are not mapped yet; custom month names were not generated.");
|
|
134
|
+
} else if (months.length !== spec.monthKeys.length) {
|
|
135
|
+
notes.push(
|
|
136
|
+
`The engine has exactly ${spec.monthKeys.length} months; your calendar declares ${months.length}, ` +
|
|
137
|
+
"so month names were not generated (the month count itself cannot be modded)."
|
|
138
|
+
);
|
|
139
|
+
} else {
|
|
140
|
+
overrideLines.push(" # Engine month names (long and abbreviated forms both get the custom name).");
|
|
141
|
+
months.forEach((m, i) => {
|
|
142
|
+
const [long, short] = spec.monthKeys![i];
|
|
143
|
+
overrideLines.push(` ${long}:0 "${m.name.replace(/"/g, '\\"')}"`);
|
|
144
|
+
if (short !== long) overrideLines.push(` ${short}:0 "${m.name.replace(/"/g, '\\"')}"`);
|
|
145
|
+
});
|
|
146
|
+
notes.push(
|
|
147
|
+
"Month day counts are engine-fixed (31/28/31...); custom day counts only affect the editor's display."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
notes.push(
|
|
153
|
+
`Generated for '${lang}' only; other languages the mod ships need the same overrides in their replace folder.`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
files: [
|
|
158
|
+
{ relPath: `localization/${lang}/px_calendar_l_${lang}.yml`, content: locFile(lang, mathLines) },
|
|
159
|
+
{
|
|
160
|
+
relPath: `localization/replace/${lang}/px_calendar_dates_l_${lang}.yml`,
|
|
161
|
+
content: locFile(lang, overrideLines),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
notes,
|
|
165
|
+
};
|
|
166
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Constants needed on both sides of the LSP boundary.
|
|
3
|
-
*/
|
|
4
|
-
import type { TokenKind } from "./types";
|
|
5
|
-
|
|
6
|
-
/** The script_docs log files and the token kind each one contributes. */
|
|
7
|
-
export const LOG_FILES: Array<{ file: string; kind: TokenKind }> = [
|
|
8
|
-
{ file: "triggers.log", kind: "trigger" },
|
|
9
|
-
{ file: "effects.log", kind: "effect" },
|
|
10
|
-
{ file: "event_targets.log", kind: "event_target" },
|
|
11
|
-
{ file: "modifiers.log", kind: "modifier" },
|
|
12
|
-
];
|
|
1
|
+
/**
|
|
2
|
+
* Constants needed on both sides of the LSP boundary.
|
|
3
|
+
*/
|
|
4
|
+
import type { TokenKind } from "./types";
|
|
5
|
+
|
|
6
|
+
/** The script_docs log files and the token kind each one contributes. */
|
|
7
|
+
export const LOG_FILES: Array<{ file: string; kind: TokenKind }> = [
|
|
8
|
+
{ file: "triggers.log", kind: "trigger" },
|
|
9
|
+
{ file: "effects.log", kind: "effect" },
|
|
10
|
+
{ file: "event_targets.log", kind: "event_target" },
|
|
11
|
+
{ file: "modifiers.log", kind: "modifier" },
|
|
12
|
+
];
|