@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/LICENSE +674 -674
- package/README.md +25 -25
- package/dist/calendar.d.ts +71 -0
- package/dist/calendar.js +182 -0
- package/dist/calendarFile.d.ts +26 -0
- package/dist/calendarFile.js +109 -0
- package/dist/calendarLoc.d.ts +60 -0
- package/dist/calendarLoc.js +101 -0
- package/dist/configDir.d.ts +17 -0
- package/dist/configDir.js +79 -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 +661 -4
- package/dist/protocol.js +159 -2
- package/dist/workshopMeta.d.ts +40 -0
- package/dist/workshopMeta.js +146 -0
- package/package.json +1 -1
- package/src/arrays.ts +16 -16
- package/src/calendar.ts +183 -0
- package/src/calendarFile.ts +81 -0
- package/src/calendarLoc.ts +159 -0
- package/src/configDir.ts +47 -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 +2156 -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 +127 -0
package/dist/protocol.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.scopeAtRequest = exports.dependenciesRequest = exports.eventValueOptionsRequest = exports.EVENT_VOCABULARY_MAX_VALUES = exports.EVENT_VOCABULARY_MAX_TOKENS = exports.eventVocabularyRequest = exports.eventBannerRequest = exports.eventGraphRequest = exports.guiWidgetEditRequest = exports.guiSourceEditRequest = exports.guiSaveValuesRequest = exports.GUI_PREVIEW_MAX = exports.guiPreviewRequest = exports.guiVocabularyRequest = exports.guiDependenciesRequest = exports.guiWidgetInfoRequest = exports.guiLayoutRequest = exports.guiTreeRequest = exports.eventDetailRequest = exports.overridesRequest = exports.locCoverageRequest = exports.modOverviewRequest = exports.progressNotification = exports.indexChangedNotification = exports.statusNotification = exports.lookupLocRequest = exports.indexStatsRequest = exports.reloadDocsRequest = exports.modFileChangedNotification = exports.configChangedNotification = exports.allClientCommandIds = exports.clientCommands = void 0;
|
|
3
|
+
exports.DYNASTY_SKILLS = exports.dynastyTreeRequest = exports.modifierFormatsRequest = exports.definitionEditRequest = exports.definitionFormRequest = exports.scopeAtRequest = exports.snippetsRequest = exports.dependenciesRequest = exports.eventValueOptionsRequest = exports.DEFINITION_FORM_MAX_EXAMPLE = exports.DEFINITION_FORM_MAX_SAMPLED = exports.EVENT_VOCABULARY_MAX_VALUES = exports.EVENT_VOCABULARY_MAX_TOKENS = exports.eventVocabularyRequest = exports.eventBannerRequest = exports.eventGraphRequest = exports.guiWidgetEditRequest = exports.guiSourceEditRequest = exports.guiSaveValuesRequest = exports.GUI_PREVIEW_MAX = exports.guiPreviewRequest = exports.guiVocabularyRequest = exports.guiDependenciesRequest = exports.guiWidgetInfoRequest = exports.guiLayoutRequest = exports.guiTreeRequest = exports.exampleWikiEntryRequest = exports.exampleWikiVocabularyKinds = exports.exampleWikiVariableKinds = exports.exampleWikiRequest = exports.eventDetailRequest = exports.overridesRequest = exports.locCoverageRequest = exports.modOverviewRequest = exports.progressNotification = exports.indexChangedNotification = exports.statusNotification = exports.locTextRequest = exports.lookupLocRequest = exports.indexStatsRequest = exports.reloadDocsRequest = exports.modFileChangedNotification = exports.configChangedNotification = exports.allClientCommandIds = exports.clientCommands = void 0;
|
|
4
4
|
// ---- client command ids ----------------------------------------------------
|
|
5
5
|
/**
|
|
6
6
|
* Client commands the server references in code actions and hover links (part
|
|
@@ -14,6 +14,7 @@ exports.clientCommands = {
|
|
|
14
14
|
editLocalization: "px.editLocalization",
|
|
15
15
|
openLocalizationSideBySide: "px.openLocalizationSideBySide",
|
|
16
16
|
showReferences: "px.showReferences",
|
|
17
|
+
showExamplesWiki: "px.showExamplesWiki",
|
|
17
18
|
};
|
|
18
19
|
/** Every id in {@link clientCommands}: what a fully capable client registers. */
|
|
19
20
|
exports.allClientCommandIds = Object.values(exports.clientCommands);
|
|
@@ -29,6 +30,24 @@ exports.indexStatsRequest = "paradox/indexStats";
|
|
|
29
30
|
/** Request: look up localization entries for a key; {@link LookupLocParams} -> {@link LocEntryInfo}[].
|
|
30
31
|
* Mod entries shadow vanilla ones (the full list is returned, mod first). */
|
|
31
32
|
exports.lookupLocRequest = "paradox/lookupLoc";
|
|
33
|
+
/**
|
|
34
|
+
* Request: a localization value as the PLAYER reads it;
|
|
35
|
+
* {@link LocTextParams} -> {@link LocTextResult}.
|
|
36
|
+
*
|
|
37
|
+
* {@link lookupLocRequest} answers the value verbatim, which is what an editor
|
|
38
|
+
* needs. A panel that SHOWS the value needs the sentence: the games write a
|
|
39
|
+
* culture parameter as `"The [GetTrait('rough_terrain_expert').GetName(
|
|
40
|
+
* GetNullCharacter )] Commander Trait is more common"` (145 of the 280
|
|
41
|
+
* parameter values with a real call take that one shape), and a modder reading
|
|
42
|
+
* a form must not be shown the brackets.
|
|
43
|
+
*
|
|
44
|
+
* Everything the renderer knows is DERIVED: the words come from the loc index
|
|
45
|
+
* (mod entries shadow the game's), the kind a `Get<Something>('name')` chain
|
|
46
|
+
* names comes from the definition index, and the loc key that kind's names take
|
|
47
|
+
* comes from the active profile's schema. No table of function names, so a
|
|
48
|
+
* workspace of any of the games gets the same behavior from its own schema.
|
|
49
|
+
*/
|
|
50
|
+
exports.locTextRequest = "paradox/locText";
|
|
32
51
|
// ---- server -> client ------------------------------------------------------
|
|
33
52
|
/** Notification: data health for the status bar; payload {@link StatusPayload}. */
|
|
34
53
|
exports.statusNotification = "paradox/status";
|
|
@@ -48,12 +67,46 @@ exports.locCoverageRequest = "paradox/locCoverage";
|
|
|
48
67
|
exports.overridesRequest = "paradox/overrides";
|
|
49
68
|
/** Request: full event detail for the graph inspector; {@link EventDetailParams} -> {@link EventDetail} | null. */
|
|
50
69
|
exports.eventDetailRequest = "paradox/eventDetail";
|
|
70
|
+
/**
|
|
71
|
+
* Request: the searchable catalog behind the Examples Wiki;
|
|
72
|
+
* `null` -> {@link ExampleWikiIndex}.
|
|
73
|
+
*
|
|
74
|
+
* One compact row per name the server knows about, so a client can filter and
|
|
75
|
+
* rank the whole vocabulary without asking again. Everything expensive (the
|
|
76
|
+
* full documentation, the usage block, the vanilla sites) is left to
|
|
77
|
+
* {@link exampleWikiEntryRequest}.
|
|
78
|
+
*/
|
|
79
|
+
exports.exampleWikiRequest = "paradox/exampleWiki";
|
|
80
|
+
/** The {@link ExampleWikiKind}s whose rows come from the definition index. */
|
|
81
|
+
exports.exampleWikiVariableKinds = [
|
|
82
|
+
"variable",
|
|
83
|
+
"local_variable",
|
|
84
|
+
"global_variable",
|
|
85
|
+
"variable_list",
|
|
86
|
+
"local_variable_list",
|
|
87
|
+
"global_variable_list",
|
|
88
|
+
"list",
|
|
89
|
+
];
|
|
90
|
+
/**
|
|
91
|
+
* The {@link ExampleWikiKind}s whose rows are script grammar rather than a
|
|
92
|
+
* name from a dump or an index. One filter chip covers both.
|
|
93
|
+
*/
|
|
94
|
+
exports.exampleWikiVocabularyKinds = ["keyword", "scope_word"];
|
|
95
|
+
/**
|
|
96
|
+
* Request: everything the toolkit knows about ONE Examples Wiki row;
|
|
97
|
+
* {@link ExampleWikiEntryParams} -> {@link ExampleWikiDetail} | null.
|
|
98
|
+
*
|
|
99
|
+
* `null` means the name is not in the catalog. Vanilla example sites are
|
|
100
|
+
* searched on demand and come back as absolute paths, so a client can open
|
|
101
|
+
* the file at the line without resolving anything itself.
|
|
102
|
+
*/
|
|
103
|
+
exports.exampleWikiEntryRequest = "paradox/exampleWikiEntry";
|
|
51
104
|
/** Request: GUI widget tree for a .gui document; {@link GuiTreeParams} -> {@link GuiTree}. */
|
|
52
105
|
exports.guiTreeRequest = "paradox/guiTree";
|
|
53
106
|
/**
|
|
54
107
|
* Request: rendered GUI layout for a .gui document;
|
|
55
108
|
* {@link GuiLayoutParams} -> {@link GuiLayoutResult}. Rectangles come from
|
|
56
|
-
* the measured layout engine (docs/gui-designer/
|
|
109
|
+
* the measured layout engine (docs/gui-designer/spec.md), with
|
|
57
110
|
* templates/types resolved against the vanilla + mod gui tree.
|
|
58
111
|
*/
|
|
59
112
|
exports.guiLayoutRequest = "paradox/guiLayout";
|
|
@@ -169,6 +222,20 @@ exports.eventVocabularyRequest = "paradox/eventVocabulary";
|
|
|
169
222
|
/** Caps: an editor lists a page at a time, and these ride on every open. */
|
|
170
223
|
exports.EVENT_VOCABULARY_MAX_TOKENS = 600;
|
|
171
224
|
exports.EVENT_VOCABULARY_MAX_VALUES = 400;
|
|
225
|
+
/**
|
|
226
|
+
* Most values {@link DefinitionFormKey.sampled} carries, and the point past
|
|
227
|
+
* which a key is taken to have no value SET at all (a key whose value differs
|
|
228
|
+
* per definition is a free field, not a list to offer).
|
|
229
|
+
*/
|
|
230
|
+
exports.DEFINITION_FORM_MAX_SAMPLED = 80;
|
|
231
|
+
/**
|
|
232
|
+
* How long a block body {@link DefinitionFormKey.example} may be. A placeholder
|
|
233
|
+
* is read at a glance, and the shortest bodies a game writes for a block key
|
|
234
|
+
* (a trait's `triggered_opinion`, a culture's `parameters`) fit well inside
|
|
235
|
+
* this; a longer one is cut with an ellipsis rather than dropped, because half
|
|
236
|
+
* a real body still says what the key wants.
|
|
237
|
+
*/
|
|
238
|
+
exports.DEFINITION_FORM_MAX_EXAMPLE = 120;
|
|
172
239
|
/**
|
|
173
240
|
* Request: the value set a VALUE belongs to, resolved through the definition
|
|
174
241
|
* index; {@link EventValueOptionsParams} -> {@link EventValueOptionsResult} |
|
|
@@ -187,6 +254,23 @@ exports.eventValueOptionsRequest = "paradox/eventValueOptions";
|
|
|
187
254
|
* (uri + position) or by name (optionally disambiguated by kind).
|
|
188
255
|
*/
|
|
189
256
|
exports.dependenciesRequest = "paradox/dependencies";
|
|
257
|
+
/**
|
|
258
|
+
* Request: the code snippets a host can offer for one open script document;
|
|
259
|
+
* {@link SnippetsParams} -> {@link SnippetsResult}. Answers for OPEN script
|
|
260
|
+
* documents only (the server reads the client's text, not the disk); a document
|
|
261
|
+
* it does not know answers with an EMPTY list, never an error.
|
|
262
|
+
*
|
|
263
|
+
* Two sources, neither hand-written. The definition and child-block skeletons
|
|
264
|
+
* are the measured shape of the document folder's own definition kind (at least
|
|
265
|
+
* half of the game's definitions of that kind carry each key, in the median
|
|
266
|
+
* order they hold there). The token entries are the block form of the `usage:`
|
|
267
|
+
* example the game's own script_docs dump ships for an engine trigger or effect,
|
|
268
|
+
* filtered to the block the cursor sits in.
|
|
269
|
+
*
|
|
270
|
+
* Every entry carries BOTH insert forms, exactly like completion does: `snippet`
|
|
271
|
+
* for a host that expands `${1:…}` tabstops, `plain` for one that does not.
|
|
272
|
+
*/
|
|
273
|
+
exports.snippetsRequest = "paradox/snippets";
|
|
190
274
|
/**
|
|
191
275
|
* Request: the inferred scope chain at a cursor position;
|
|
192
276
|
* {@link ScopeAtParams} -> {@link ScopeAtResult} | null. Answers for OPEN
|
|
@@ -199,3 +283,76 @@ exports.dependenciesRequest = "paradox/dependencies";
|
|
|
199
283
|
* asserts more than the derived link tables actually say.
|
|
200
284
|
*/
|
|
201
285
|
exports.scopeAtRequest = "paradox/scopeAt";
|
|
286
|
+
// ---- content creators --------------------------------------------------------
|
|
287
|
+
/**
|
|
288
|
+
* Request: everything a visual creator needs to draw a form for one definition
|
|
289
|
+
* kind; {@link DefinitionFormParams} -> {@link DefinitionForm} | null (null =
|
|
290
|
+
* the active game's schema has no such kind, which is the honest answer for a
|
|
291
|
+
* client asking about content this game does not have).
|
|
292
|
+
*
|
|
293
|
+
* Nothing in the answer is hand-written for the creator: the folder, the loc
|
|
294
|
+
* key patterns and the icon folder come from the schema table, the keys from
|
|
295
|
+
* the harvested `_*.info` structures, the option lists from the definition
|
|
296
|
+
* index (the same resolver {@link eventValueOptionsRequest} answers with) and
|
|
297
|
+
* `existing` from the same index walk {@link modOverviewRequest} does. A game
|
|
298
|
+
* patch that adds a key or a value changes the form without a release.
|
|
299
|
+
*/
|
|
300
|
+
exports.definitionFormRequest = "paradox/definitionForm";
|
|
301
|
+
/**
|
|
302
|
+
* Request: text edits that write a definition into a script file;
|
|
303
|
+
* {@link DefinitionEditParams} -> {@link DefinitionEditResult}. The script
|
|
304
|
+
* sibling of {@link guiSourceEditRequest}, over the same span model, and with
|
|
305
|
+
* the same division of labour: the server never writes, it returns offsets
|
|
306
|
+
* into the text it was handed and the host applies them as ONE
|
|
307
|
+
* `WorkspaceEdit`, which keeps undo and dirty state in the editor.
|
|
308
|
+
*
|
|
309
|
+
* Offsets are UTF-16 into `params.text` (the document text, with no BOM, the
|
|
310
|
+
* way an editor delivers it), computed against that one text and applied
|
|
311
|
+
* end-first. Every edit is surgical, so a file's other definitions, its
|
|
312
|
+
* comments, its CRLF and its indentation stay byte-identical.
|
|
313
|
+
*/
|
|
314
|
+
exports.definitionEditRequest = "paradox/definitionEdit";
|
|
315
|
+
/**
|
|
316
|
+
* Request: how the GAME prints each modifier; {@link ModifierFormatsParams} ->
|
|
317
|
+
* {@link ModifierFormatsResult} | null (null = the active profile names no
|
|
318
|
+
* formats source, or the game folder is not configured).
|
|
319
|
+
*
|
|
320
|
+
* A creator that lets a modder add `monthly_income = 0.5` has to show what the
|
|
321
|
+
* player will see, and the player sees "[gold_i] +0.50 Monthly Income" in
|
|
322
|
+
* green. None of that is written here: the flags come from the game's own
|
|
323
|
+
* `common/modifier_definition_formats/` (documented by `_definitions.info`
|
|
324
|
+
* there), every word comes from the loc index, and every icon comes from the
|
|
325
|
+
* `texticon` blocks of the game's `gui/texticons.gui`. A modifier no format
|
|
326
|
+
* block names gets the file's documented defaults, so the answer covers every
|
|
327
|
+
* modifier token the server knows rather than only the formatted ones.
|
|
328
|
+
*/
|
|
329
|
+
exports.modifierFormatsRequest = "paradox/modifierFormats";
|
|
330
|
+
/**
|
|
331
|
+
* Request: a dynasty as a family tree; {@link DynastyTreeParams} ->
|
|
332
|
+
* {@link DynastyTreeResult}.
|
|
333
|
+
*
|
|
334
|
+
* Two answers behind one method. Without `dynasty` the result is the picker
|
|
335
|
+
* list: every dynasty the index knows, mod entries first. With `dynasty` it is
|
|
336
|
+
* that dynasty's houses and members, read out of the game's own
|
|
337
|
+
* `history/characters` files.
|
|
338
|
+
*
|
|
339
|
+
* Everything is DERIVED: the folders come from the active profile's schema
|
|
340
|
+
* (`dynasty`, `dynasty_house`, `character` kinds), the members from the
|
|
341
|
+
* character blocks themselves, the display names from the loc index. A profile
|
|
342
|
+
* whose schema has no `dynasty` kind answers `supported: false` and empty
|
|
343
|
+
* lists, which is what a client shows instead of an empty tree.
|
|
344
|
+
*/
|
|
345
|
+
exports.dynastyTreeRequest = "paradox/dynastyTree";
|
|
346
|
+
/**
|
|
347
|
+
* The character-level skill keys, in the order a client shows them. MEASURED
|
|
348
|
+
* over the vanilla `history/characters` corpus (2026-09-03): stewardship 8 964,
|
|
349
|
+
* martial 8 940, diplomacy 8 908, intrigue 8 892, learning 495, prowess 150.
|
|
350
|
+
*/
|
|
351
|
+
exports.DYNASTY_SKILLS = [
|
|
352
|
+
"diplomacy",
|
|
353
|
+
"martial",
|
|
354
|
+
"stewardship",
|
|
355
|
+
"intrigue",
|
|
356
|
+
"learning",
|
|
357
|
+
"prowess",
|
|
358
|
+
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Title/description pair of one Workshop language. Absent field = not translated. */
|
|
2
|
+
export interface WorkshopTranslation {
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
/** The fields of `workshop.json` this toolkit reads or writes. */
|
|
7
|
+
export interface WorkshopMeta {
|
|
8
|
+
/** Workshop item id (decimal string), for the games whose descriptor has no field for it. */
|
|
9
|
+
publishedFileId?: string;
|
|
10
|
+
/** The item's description in the default language, BBCode as Steam renders it. */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Keyed by Steam API language code (`german`, `schinese`, ...), never the default language. */
|
|
13
|
+
translations?: Record<string, WorkshopTranslation>;
|
|
14
|
+
}
|
|
15
|
+
/** The parsed `<configDir>/workshop.json`, or null when absent/unreadable. */
|
|
16
|
+
export declare function readWorkshopMeta(configDir: string): WorkshopMeta | null;
|
|
17
|
+
/**
|
|
18
|
+
* Merge `patch` into the record and write it back. Unknown keys of the file
|
|
19
|
+
* survive; a patch key set to `undefined` is left as it was. `translations`
|
|
20
|
+
* replaces as a whole (the caller edits the full map).
|
|
21
|
+
*/
|
|
22
|
+
export declare function upsertWorkshopMeta(configDir: string, patch: WorkshopMeta): void;
|
|
23
|
+
/**
|
|
24
|
+
* The languages the Workshop accepts item text in: Steam's API language codes
|
|
25
|
+
* with their English names, in Steam's documented order.
|
|
26
|
+
* https://partner.steamgames.com/doc/store/localization/languages
|
|
27
|
+
*/
|
|
28
|
+
export declare const STEAM_LANGUAGES: readonly {
|
|
29
|
+
api: string;
|
|
30
|
+
label: string;
|
|
31
|
+
}[];
|
|
32
|
+
/** English name of a Steam API language code; the code itself when unknown. */
|
|
33
|
+
export declare function steamLanguageLabel(api: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Steam API language code for a Paradox localization folder language
|
|
36
|
+
* (`translationCore.ts` LOC_LANGUAGES), or null when Steam has no counterpart.
|
|
37
|
+
* The two vocabularies differ where Steam's codes predate its own store pages
|
|
38
|
+
* (`koreana`, `schinese`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function steamLanguageForLoc(locLanguage: string): string | null;
|
|
@@ -0,0 +1,146 @@
|
|
|
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.readWorkshopMeta = readWorkshopMeta;
|
|
38
|
+
exports.upsertWorkshopMeta = upsertWorkshopMeta;
|
|
39
|
+
exports.steamLanguageLabel = steamLanguageLabel;
|
|
40
|
+
exports.steamLanguageForLoc = steamLanguageForLoc;
|
|
41
|
+
/**
|
|
42
|
+
* The mod's local Workshop record: `<mod>/<configDir>/workshop.json`. It holds
|
|
43
|
+
* what the game's own descriptor has no field for - the item's description and
|
|
44
|
+
* its per-language translations (title + description shown to Workshop
|
|
45
|
+
* visitors browsing Steam in that language), plus the published id for the
|
|
46
|
+
* `.metadata` games (launcher-`.mod` games keep the id in `remote_file_id`).
|
|
47
|
+
*
|
|
48
|
+
* The local file is the canonical copy once the user manages the item from the
|
|
49
|
+
* toolkit: uploads read from here, and "fetch" pulls the live values down into
|
|
50
|
+
* it. Reads and writes are merge-preserving: keys this version does not know
|
|
51
|
+
* survive a round trip.
|
|
52
|
+
*
|
|
53
|
+
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
54
|
+
*/
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
/** The parsed `<configDir>/workshop.json`, or null when absent/unreadable. */
|
|
58
|
+
function readWorkshopMeta(configDir) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = JSON.parse(fs.readFileSync(path.join(configDir, "workshop.json"), "utf8"));
|
|
61
|
+
if (typeof raw !== "object" || raw === null)
|
|
62
|
+
return null;
|
|
63
|
+
return raw;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Merge `patch` into the record and write it back. Unknown keys of the file
|
|
71
|
+
* survive; a patch key set to `undefined` is left as it was. `translations`
|
|
72
|
+
* replaces as a whole (the caller edits the full map).
|
|
73
|
+
*/
|
|
74
|
+
function upsertWorkshopMeta(configDir, patch) {
|
|
75
|
+
const file = path.join(configDir, "workshop.json");
|
|
76
|
+
const current = (readWorkshopMeta(configDir) ?? {});
|
|
77
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
78
|
+
if (value !== undefined)
|
|
79
|
+
current[key] = value;
|
|
80
|
+
}
|
|
81
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
82
|
+
fs.writeFileSync(file, JSON.stringify(current, null, 2) + "\n", "utf8");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The languages the Workshop accepts item text in: Steam's API language codes
|
|
86
|
+
* with their English names, in Steam's documented order.
|
|
87
|
+
* https://partner.steamgames.com/doc/store/localization/languages
|
|
88
|
+
*/
|
|
89
|
+
exports.STEAM_LANGUAGES = [
|
|
90
|
+
{ api: "arabic", label: "Arabic" },
|
|
91
|
+
{ api: "bulgarian", label: "Bulgarian" },
|
|
92
|
+
{ api: "schinese", label: "Chinese (Simplified)" },
|
|
93
|
+
{ api: "tchinese", label: "Chinese (Traditional)" },
|
|
94
|
+
{ api: "czech", label: "Czech" },
|
|
95
|
+
{ api: "danish", label: "Danish" },
|
|
96
|
+
{ api: "dutch", label: "Dutch" },
|
|
97
|
+
{ api: "english", label: "English" },
|
|
98
|
+
{ api: "finnish", label: "Finnish" },
|
|
99
|
+
{ api: "french", label: "French" },
|
|
100
|
+
{ api: "german", label: "German" },
|
|
101
|
+
{ api: "greek", label: "Greek" },
|
|
102
|
+
{ api: "hungarian", label: "Hungarian" },
|
|
103
|
+
{ api: "indonesian", label: "Indonesian" },
|
|
104
|
+
{ api: "italian", label: "Italian" },
|
|
105
|
+
{ api: "japanese", label: "Japanese" },
|
|
106
|
+
{ api: "koreana", label: "Korean" },
|
|
107
|
+
{ api: "norwegian", label: "Norwegian" },
|
|
108
|
+
{ api: "polish", label: "Polish" },
|
|
109
|
+
{ api: "portuguese", label: "Portuguese" },
|
|
110
|
+
{ api: "brazilian", label: "Portuguese (Brazil)" },
|
|
111
|
+
{ api: "romanian", label: "Romanian" },
|
|
112
|
+
{ api: "russian", label: "Russian" },
|
|
113
|
+
{ api: "spanish", label: "Spanish (Spain)" },
|
|
114
|
+
{ api: "latam", label: "Spanish (Latin America)" },
|
|
115
|
+
{ api: "swedish", label: "Swedish" },
|
|
116
|
+
{ api: "thai", label: "Thai" },
|
|
117
|
+
{ api: "turkish", label: "Turkish" },
|
|
118
|
+
{ api: "ukrainian", label: "Ukrainian" },
|
|
119
|
+
{ api: "vietnamese", label: "Vietnamese" },
|
|
120
|
+
];
|
|
121
|
+
/** English name of a Steam API language code; the code itself when unknown. */
|
|
122
|
+
function steamLanguageLabel(api) {
|
|
123
|
+
return exports.STEAM_LANGUAGES.find((l) => l.api === api)?.label ?? api;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Steam API language code for a Paradox localization folder language
|
|
127
|
+
* (`translationCore.ts` LOC_LANGUAGES), or null when Steam has no counterpart.
|
|
128
|
+
* The two vocabularies differ where Steam's codes predate its own store pages
|
|
129
|
+
* (`koreana`, `schinese`).
|
|
130
|
+
*/
|
|
131
|
+
function steamLanguageForLoc(locLanguage) {
|
|
132
|
+
const map = {
|
|
133
|
+
english: "english",
|
|
134
|
+
french: "french",
|
|
135
|
+
german: "german",
|
|
136
|
+
spanish: "spanish",
|
|
137
|
+
russian: "russian",
|
|
138
|
+
korean: "koreana",
|
|
139
|
+
simp_chinese: "schinese",
|
|
140
|
+
japanese: "japanese",
|
|
141
|
+
polish: "polish",
|
|
142
|
+
braz_por: "brazilian",
|
|
143
|
+
turkish: "turkish",
|
|
144
|
+
};
|
|
145
|
+
return map[locLanguage] ?? null;
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@px-lsp/protocol",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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,183 @@
|
|
|
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 CalendarSetting {
|
|
15
|
+
/** Script year displayed as year 1 of the `after` era (no year zero). */
|
|
16
|
+
epoch: number;
|
|
17
|
+
/** Era label for script years >= epoch ("AD", "TA"...). */
|
|
18
|
+
after: string;
|
|
19
|
+
/** Era label for script years < epoch ("BC"). Omitted = single-era
|
|
20
|
+
* calendar: years before the epoch get no display form. */
|
|
21
|
+
before?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The engine's twelve months under the mod's own names, first month first.
|
|
24
|
+
* Omitted = January to December. Only the NAMES are the mod's: the game has
|
|
25
|
+
* twelve months of fixed length (31 28 31 30 31 30 31 31 30 31 30 31, no
|
|
26
|
+
* leap years) and no script can change that, so a date's month and day are
|
|
27
|
+
* always the engine's and only read differently.
|
|
28
|
+
*/
|
|
29
|
+
months?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Days per engine month; what a script date's day is bounded by. */
|
|
33
|
+
export const ENGINE_MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
34
|
+
|
|
35
|
+
export const GREGORIAN_MONTHS: string[] = [
|
|
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
|
+
];
|
|
49
|
+
|
|
50
|
+
/** The month names a date reads with: the mod's twelve, or the engine's. */
|
|
51
|
+
export function monthNames(cal: CalendarSetting): string[] {
|
|
52
|
+
return cal.months ?? GREGORIAN_MONTHS;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate a calendar straight out of JSON settings (any client, any hand-
|
|
57
|
+
* edited settings file). Returns a clean copy, or undefined when the value is
|
|
58
|
+
* not a usable calendar - features then behave as if none was configured.
|
|
59
|
+
*/
|
|
60
|
+
export function sanitizeCalendar(raw: unknown): CalendarSetting | undefined {
|
|
61
|
+
if (typeof raw !== "object" || raw === null) return undefined;
|
|
62
|
+
const o = raw as Record<string, unknown>;
|
|
63
|
+
const epoch = o.epoch;
|
|
64
|
+
const after = o.after;
|
|
65
|
+
if (typeof epoch !== "number" || !Number.isInteger(epoch) || epoch < 1) return undefined;
|
|
66
|
+
if (typeof after !== "string" || after.trim() === "") return undefined;
|
|
67
|
+
const cal: CalendarSetting = { epoch, after: after.trim() };
|
|
68
|
+
if (typeof o.before === "string" && o.before.trim() !== "") cal.before = o.before.trim();
|
|
69
|
+
// Typed input picks the era by its label and the month by its name, so a
|
|
70
|
+
// collision would resolve silently to the wrong one: not a usable calendar.
|
|
71
|
+
if (cal.before && cal.before.toLowerCase() === cal.after.toLowerCase()) return undefined;
|
|
72
|
+
if (Array.isArray(o.months) && o.months.length > 0) {
|
|
73
|
+
// Exactly the engine's twelve. A name may still arrive as the older
|
|
74
|
+
// `{ name, days }` object; its day count never meant anything to the
|
|
75
|
+
// game and is dropped.
|
|
76
|
+
if (o.months.length !== GREGORIAN_MONTHS.length) return undefined;
|
|
77
|
+
const months: string[] = [];
|
|
78
|
+
const seen = new Set<string>();
|
|
79
|
+
for (const m of o.months) {
|
|
80
|
+
const name = typeof m === "string" ? m : ((m as Record<string, unknown> | null)?.name ?? null);
|
|
81
|
+
if (typeof name !== "string" || name.trim() === "") return undefined;
|
|
82
|
+
const key = name.trim().toLowerCase();
|
|
83
|
+
if (seen.has(key)) return undefined;
|
|
84
|
+
seen.add(key);
|
|
85
|
+
months.push(name.trim());
|
|
86
|
+
}
|
|
87
|
+
cal.months = months;
|
|
88
|
+
}
|
|
89
|
+
return cal;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A script-file date token (`3000.1.1`), or null when the text is not one. */
|
|
93
|
+
export function parseScriptDate(text: string): { y: number; m: number; d: number } | null {
|
|
94
|
+
const match = /^(\d{1,5})\.(\d{1,2})\.(\d{1,2})$/.exec(text);
|
|
95
|
+
if (!match) return null;
|
|
96
|
+
return { y: Number(match[1]), m: Number(match[2]), d: Number(match[3]) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A date the engine reads: a positive year, one of its twelve months, a day that month has. */
|
|
100
|
+
export function isValidScriptDate(y: number, m: number, d: number): boolean {
|
|
101
|
+
return y >= 1 && m >= 1 && m <= ENGINE_MONTH_DAYS.length && d >= 1 && d <= ENGINE_MONTH_DAYS[m - 1];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Era-mapped year: "1000 BC". Null for pre-epoch years of a single-era calendar. */
|
|
105
|
+
export function displayYear(cal: CalendarSetting, y: number): string | null {
|
|
106
|
+
if (y >= cal.epoch) return `${y - cal.epoch + 1} ${cal.after}`;
|
|
107
|
+
return cal.before ? `${cal.epoch - y} ${cal.before}` : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Full display form of a script date: "1000 BC" for the year's first day
|
|
112
|
+
* (how start dates read in game), "15 March 1000 BC" otherwise. Null when the
|
|
113
|
+
* date does not fit the calendar.
|
|
114
|
+
*/
|
|
115
|
+
export function displayDate(cal: CalendarSetting, y: number, m: number, d: number): string | null {
|
|
116
|
+
if (!isValidScriptDate(y, m, d)) return null;
|
|
117
|
+
const year = displayYear(cal, y);
|
|
118
|
+
if (!year) return null;
|
|
119
|
+
if (m === 1 && d === 1) return year;
|
|
120
|
+
return `${d} ${monthNames(cal)[m - 1]} ${year}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type ConvertResult = { ok: true; script: string; display: string } | { ok: false; error: string };
|
|
124
|
+
|
|
125
|
+
/** Case-insensitive month lookup: exact name, else unique prefix. */
|
|
126
|
+
function monthByName(cal: CalendarSetting, text: string): number | null {
|
|
127
|
+
const needle = text.toLowerCase();
|
|
128
|
+
const months = monthNames(cal);
|
|
129
|
+
const exact = months.findIndex((m) => m.toLowerCase() === needle);
|
|
130
|
+
if (exact >= 0) return exact + 1;
|
|
131
|
+
const prefixed = months.map((m, i) => ({ m, i })).filter(({ m }) => m.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(y, m, d)) {
|
|
178
|
+
return m >= 1 && m <= ENGINE_MONTH_DAYS.length
|
|
179
|
+
? { ok: false, error: `${monthNames(cal)[m - 1]} has ${ENGINE_MONTH_DAYS[m - 1]} days` }
|
|
180
|
+
: { ok: false, error: `the game has ${ENGINE_MONTH_DAYS.length} months` };
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, script: `${y}.${m}.${d}`, display: displayDate(cal, y, m, d)! };
|
|
183
|
+
}
|