@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/src/translationCore.ts
CHANGED
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure logic for the translation scaffolding workflow: mirror the structure of
|
|
3
|
-
* an existing localization (usually english) into a new language so a
|
|
4
|
-
* translator only has to replace values.
|
|
5
|
-
*
|
|
6
|
-
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
7
|
-
*/
|
|
8
|
-
import * as path from "path";
|
|
9
|
-
|
|
10
|
-
export const LOC_LANGUAGES = [
|
|
11
|
-
"english",
|
|
12
|
-
"french",
|
|
13
|
-
"german",
|
|
14
|
-
"spanish",
|
|
15
|
-
"russian",
|
|
16
|
-
"korean",
|
|
17
|
-
"simp_chinese",
|
|
18
|
-
"japanese",
|
|
19
|
-
"polish",
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
const BOM = "";
|
|
23
|
-
const HEADER = /^(\s*)l_([a-z_]+):/m;
|
|
24
|
-
const ENTRY = /^\s*([A-Za-z0-9_.\-']+):\d*\s*"/;
|
|
25
|
-
|
|
26
|
-
/** Language of a loc file, from its `_l_<lang>.yml` suffix or a path segment. */
|
|
27
|
-
export function detectLocFileLanguage(filePath: string): string | null {
|
|
28
|
-
const m = /_l_([a-z_]+)\.ya?ml$/i.exec(filePath);
|
|
29
|
-
if (m) return m[1].toLowerCase();
|
|
30
|
-
const segments = filePath.toLowerCase().split(/[\\/]/);
|
|
31
|
-
for (const lang of LOC_LANGUAGES) {
|
|
32
|
-
if (segments.includes(lang)) return lang;
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Where the translated counterpart of `srcFile` lives: language path segments
|
|
39
|
-
* and the `_l_<lang>` filename marker are retargeted. Returns null when the
|
|
40
|
-
* path carries no language marker at all.
|
|
41
|
-
*/
|
|
42
|
-
export function retargetLocPath(srcFile: string, sourceLang: string, targetLang: string): string | null {
|
|
43
|
-
const parts = srcFile.split(/([\\/])/); // keep separators
|
|
44
|
-
let changed = false;
|
|
45
|
-
const out = parts.map((p) => {
|
|
46
|
-
if (p.toLowerCase() === sourceLang) {
|
|
47
|
-
changed = true;
|
|
48
|
-
return targetLang;
|
|
49
|
-
}
|
|
50
|
-
return p;
|
|
51
|
-
});
|
|
52
|
-
let result = out.join("");
|
|
53
|
-
const marker = new RegExp(`_l_${sourceLang}(\\.ya?ml)$`, "i");
|
|
54
|
-
if (marker.test(path.basename(result))) {
|
|
55
|
-
result = result.replace(marker, `_l_${targetLang}$1`);
|
|
56
|
-
changed = true;
|
|
57
|
-
}
|
|
58
|
-
return changed ? result : null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Greedy `(.*)` closes the value at the LAST quote before any trailing
|
|
62
|
-
// comment, matching the game's parsing of inner quotes (`""speech""`).
|
|
63
|
-
const ENTRY_LINE = /^(\s*[A-Za-z0-9_.\-']+:\d*\s*)"(.*)"\s*(#.*)?$/;
|
|
64
|
-
|
|
65
|
-
/** Blank an entry's value, keeping the source text visible as a comment. */
|
|
66
|
-
function blankEntry(line: string, sourceLang: string): string {
|
|
67
|
-
const m = ENTRY_LINE.exec(line);
|
|
68
|
-
if (!m || m[2] === "") return line;
|
|
69
|
-
// The game reads the value up to the last quote on the LINE, so any quote
|
|
70
|
-
// echoed into the comment would leak back into the value; downgrade to '.
|
|
71
|
-
const comment = `# ${sourceLang}: ${m[2]}${m[3] ? ` ${m[3]}` : ""}`.replace(/"/g, "'");
|
|
72
|
-
return `${m[1]}"" ${comment}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* A translation skeleton: the source file's structure (comments and blank
|
|
77
|
-
* lines preserved — they are context for the translator) with the language
|
|
78
|
-
* header switched and every value BLANKED; the source text stays visible as
|
|
79
|
-
* an inline `# english: …` comment so the translator sees it right there
|
|
80
|
-
* without it leaking into the game as a fake translation.
|
|
81
|
-
*/
|
|
82
|
-
export function buildTranslation(sourceContent: string, targetLang: string, sourceLang = "english"): string {
|
|
83
|
-
const hadBom = sourceContent.startsWith(BOM);
|
|
84
|
-
let body = hadBom ? sourceContent.slice(1) : sourceContent;
|
|
85
|
-
if (HEADER.test(body)) {
|
|
86
|
-
body = body.replace(HEADER, `$1l_${targetLang}:`);
|
|
87
|
-
} else {
|
|
88
|
-
body = `l_${targetLang}:\n` + body;
|
|
89
|
-
}
|
|
90
|
-
body = body
|
|
91
|
-
.split(/\r?\n/)
|
|
92
|
-
.map((l) => blankEntry(l, sourceLang))
|
|
93
|
-
.join("\n");
|
|
94
|
-
return BOM + body;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface MergeResult {
|
|
98
|
-
content: string;
|
|
99
|
-
added: number;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Add entries that exist in the source but not yet in the target, appended at
|
|
104
|
-
* the end under a marker comment. Existing target lines are never touched.
|
|
105
|
-
*/
|
|
106
|
-
export function mergeTranslation(
|
|
107
|
-
targetContent: string,
|
|
108
|
-
sourceContent: string,
|
|
109
|
-
sourceLang: string
|
|
110
|
-
): MergeResult {
|
|
111
|
-
const hadBom = targetContent.startsWith(BOM);
|
|
112
|
-
const target = hadBom ? targetContent.slice(1) : targetContent;
|
|
113
|
-
const eol = target.includes("\r\n") ? "\r\n" : "\n";
|
|
114
|
-
|
|
115
|
-
const existing = new Set<string>();
|
|
116
|
-
for (const line of target.split(/\r?\n/)) {
|
|
117
|
-
const m = ENTRY.exec(line);
|
|
118
|
-
if (m) existing.add(m[1]);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const missing: string[] = [];
|
|
122
|
-
for (const line of sourceContent.replace(/^/, "").split(/\r?\n/)) {
|
|
123
|
-
const m = ENTRY.exec(line);
|
|
124
|
-
if (m && !existing.has(m[1])) {
|
|
125
|
-
missing.push(blankEntry(line.replace(/\r$/, ""), sourceLang));
|
|
126
|
-
existing.add(m[1]);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (missing.length === 0) return { content: targetContent, added: 0 };
|
|
130
|
-
|
|
131
|
-
const lines = target.split(/\r?\n/);
|
|
132
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
133
|
-
lines.push(
|
|
134
|
-
"",
|
|
135
|
-
` # --- entries missing from this language; ${sourceLang} text in the comments ---`,
|
|
136
|
-
...missing,
|
|
137
|
-
""
|
|
138
|
-
);
|
|
139
|
-
return { content: (hadBom ? BOM : "") + lines.join(eol), added: missing.length };
|
|
140
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic for the translation scaffolding workflow: mirror the structure of
|
|
3
|
+
* an existing localization (usually english) into a new language so a
|
|
4
|
+
* translator only has to replace values.
|
|
5
|
+
*
|
|
6
|
+
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
7
|
+
*/
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
|
|
10
|
+
export const LOC_LANGUAGES = [
|
|
11
|
+
"english",
|
|
12
|
+
"french",
|
|
13
|
+
"german",
|
|
14
|
+
"spanish",
|
|
15
|
+
"russian",
|
|
16
|
+
"korean",
|
|
17
|
+
"simp_chinese",
|
|
18
|
+
"japanese",
|
|
19
|
+
"polish",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const BOM = "";
|
|
23
|
+
const HEADER = /^(\s*)l_([a-z_]+):/m;
|
|
24
|
+
const ENTRY = /^\s*([A-Za-z0-9_.\-']+):\d*\s*"/;
|
|
25
|
+
|
|
26
|
+
/** Language of a loc file, from its `_l_<lang>.yml` suffix or a path segment. */
|
|
27
|
+
export function detectLocFileLanguage(filePath: string): string | null {
|
|
28
|
+
const m = /_l_([a-z_]+)\.ya?ml$/i.exec(filePath);
|
|
29
|
+
if (m) return m[1].toLowerCase();
|
|
30
|
+
const segments = filePath.toLowerCase().split(/[\\/]/);
|
|
31
|
+
for (const lang of LOC_LANGUAGES) {
|
|
32
|
+
if (segments.includes(lang)) return lang;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Where the translated counterpart of `srcFile` lives: language path segments
|
|
39
|
+
* and the `_l_<lang>` filename marker are retargeted. Returns null when the
|
|
40
|
+
* path carries no language marker at all.
|
|
41
|
+
*/
|
|
42
|
+
export function retargetLocPath(srcFile: string, sourceLang: string, targetLang: string): string | null {
|
|
43
|
+
const parts = srcFile.split(/([\\/])/); // keep separators
|
|
44
|
+
let changed = false;
|
|
45
|
+
const out = parts.map((p) => {
|
|
46
|
+
if (p.toLowerCase() === sourceLang) {
|
|
47
|
+
changed = true;
|
|
48
|
+
return targetLang;
|
|
49
|
+
}
|
|
50
|
+
return p;
|
|
51
|
+
});
|
|
52
|
+
let result = out.join("");
|
|
53
|
+
const marker = new RegExp(`_l_${sourceLang}(\\.ya?ml)$`, "i");
|
|
54
|
+
if (marker.test(path.basename(result))) {
|
|
55
|
+
result = result.replace(marker, `_l_${targetLang}$1`);
|
|
56
|
+
changed = true;
|
|
57
|
+
}
|
|
58
|
+
return changed ? result : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Greedy `(.*)` closes the value at the LAST quote before any trailing
|
|
62
|
+
// comment, matching the game's parsing of inner quotes (`""speech""`).
|
|
63
|
+
const ENTRY_LINE = /^(\s*[A-Za-z0-9_.\-']+:\d*\s*)"(.*)"\s*(#.*)?$/;
|
|
64
|
+
|
|
65
|
+
/** Blank an entry's value, keeping the source text visible as a comment. */
|
|
66
|
+
function blankEntry(line: string, sourceLang: string): string {
|
|
67
|
+
const m = ENTRY_LINE.exec(line);
|
|
68
|
+
if (!m || m[2] === "") return line;
|
|
69
|
+
// The game reads the value up to the last quote on the LINE, so any quote
|
|
70
|
+
// echoed into the comment would leak back into the value; downgrade to '.
|
|
71
|
+
const comment = `# ${sourceLang}: ${m[2]}${m[3] ? ` ${m[3]}` : ""}`.replace(/"/g, "'");
|
|
72
|
+
return `${m[1]}"" ${comment}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A translation skeleton: the source file's structure (comments and blank
|
|
77
|
+
* lines preserved — they are context for the translator) with the language
|
|
78
|
+
* header switched and every value BLANKED; the source text stays visible as
|
|
79
|
+
* an inline `# english: …` comment so the translator sees it right there
|
|
80
|
+
* without it leaking into the game as a fake translation.
|
|
81
|
+
*/
|
|
82
|
+
export function buildTranslation(sourceContent: string, targetLang: string, sourceLang = "english"): string {
|
|
83
|
+
const hadBom = sourceContent.startsWith(BOM);
|
|
84
|
+
let body = hadBom ? sourceContent.slice(1) : sourceContent;
|
|
85
|
+
if (HEADER.test(body)) {
|
|
86
|
+
body = body.replace(HEADER, `$1l_${targetLang}:`);
|
|
87
|
+
} else {
|
|
88
|
+
body = `l_${targetLang}:\n` + body;
|
|
89
|
+
}
|
|
90
|
+
body = body
|
|
91
|
+
.split(/\r?\n/)
|
|
92
|
+
.map((l) => blankEntry(l, sourceLang))
|
|
93
|
+
.join("\n");
|
|
94
|
+
return BOM + body;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface MergeResult {
|
|
98
|
+
content: string;
|
|
99
|
+
added: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Add entries that exist in the source but not yet in the target, appended at
|
|
104
|
+
* the end under a marker comment. Existing target lines are never touched.
|
|
105
|
+
*/
|
|
106
|
+
export function mergeTranslation(
|
|
107
|
+
targetContent: string,
|
|
108
|
+
sourceContent: string,
|
|
109
|
+
sourceLang: string
|
|
110
|
+
): MergeResult {
|
|
111
|
+
const hadBom = targetContent.startsWith(BOM);
|
|
112
|
+
const target = hadBom ? targetContent.slice(1) : targetContent;
|
|
113
|
+
const eol = target.includes("\r\n") ? "\r\n" : "\n";
|
|
114
|
+
|
|
115
|
+
const existing = new Set<string>();
|
|
116
|
+
for (const line of target.split(/\r?\n/)) {
|
|
117
|
+
const m = ENTRY.exec(line);
|
|
118
|
+
if (m) existing.add(m[1]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const missing: string[] = [];
|
|
122
|
+
for (const line of sourceContent.replace(/^/, "").split(/\r?\n/)) {
|
|
123
|
+
const m = ENTRY.exec(line);
|
|
124
|
+
if (m && !existing.has(m[1])) {
|
|
125
|
+
missing.push(blankEntry(line.replace(/\r$/, ""), sourceLang));
|
|
126
|
+
existing.add(m[1]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (missing.length === 0) return { content: targetContent, added: 0 };
|
|
130
|
+
|
|
131
|
+
const lines = target.split(/\r?\n/);
|
|
132
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
133
|
+
lines.push(
|
|
134
|
+
"",
|
|
135
|
+
` # --- entries missing from this language; ${sourceLang} text in the comments ---`,
|
|
136
|
+
...missing,
|
|
137
|
+
""
|
|
138
|
+
);
|
|
139
|
+
return { content: (hadBom ? BOM : "") + lines.join(eol), added: missing.length };
|
|
140
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
/** Shared data model. Keep this module free of `vscode` imports: it is used by unit-tested code. */
|
|
2
|
-
|
|
3
|
-
export interface IndexStats {
|
|
4
|
-
total: number;
|
|
5
|
-
files: number;
|
|
6
|
-
byKind: Record<string, number>;
|
|
7
|
-
bySource: Record<string, number>;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type TokenKind = "trigger" | "effect" | "event_target" | "modifier";
|
|
11
|
-
|
|
12
|
-
/** One engine token parsed from a script_docs log file. */
|
|
13
|
-
export interface TokenData {
|
|
14
|
-
name: string;
|
|
15
|
-
kind: TokenKind;
|
|
16
|
-
/** Description text from the log; may be empty. */
|
|
17
|
-
doc: string;
|
|
18
|
-
/** Supported scopes as raw strings, display-only in v1. */
|
|
19
|
-
scopes: string[];
|
|
20
|
-
/** Extra metadata lines (targets, traits, categories...), display-only. */
|
|
21
|
-
traits?: string;
|
|
22
|
-
/** A syntax/usage example block (`add_hook = { type = X ... }`), preserved
|
|
23
|
-
* verbatim from a `usage:` section, an inline `name = …` line, or the wiki. */
|
|
24
|
-
usage?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Definition kinds are open strings driven by the schema table
|
|
29
|
-
* (packages/server/src/schema): "scripted_effect", "trait", "decision", ...
|
|
30
|
-
*/
|
|
31
|
-
export type DefKind = string;
|
|
32
|
-
|
|
33
|
-
/** Where a definition comes from; mod shadows parent shadows vanilla. */
|
|
34
|
-
export type DefSource = "vanilla" | "parent" | "mod";
|
|
35
|
-
|
|
36
|
-
/** One user-defined or vanilla definition found by the indexer. */
|
|
37
|
-
export interface Definition {
|
|
38
|
-
name: string;
|
|
39
|
-
kind: DefKind;
|
|
40
|
-
/** Absolute path. */
|
|
41
|
-
file: string;
|
|
42
|
-
/** 0-based line number (VS Code convention). */
|
|
43
|
-
line: number;
|
|
44
|
-
source: DefSource;
|
|
45
|
-
/** For loc_key: the localized text (truncated for memory; the edit flow re-reads the yml). */
|
|
46
|
-
value?: string;
|
|
47
|
-
/** Enclosing definition, when meaningful (e.g. the event a save_scope_as sits in). */
|
|
48
|
-
container?: string;
|
|
49
|
-
/** For scripted effects/triggers: $PARAM$ names in declaration order (signature help). */
|
|
50
|
-
params?: string[];
|
|
51
|
-
/** PdxDoc prose from a leading `#` comment block (§E); capped for memory. */
|
|
52
|
-
doc?: string;
|
|
53
|
-
/** PdxDoc structured tags (@scope, @param, @saves, @returns, @example, @deprecated, …). */
|
|
54
|
-
tags?: DocTag[];
|
|
55
|
-
/**
|
|
56
|
-
* Database entry mode stripped from the declaration key (`REPLACE:name`),
|
|
57
|
-
* for games whose profile declares entryModes. The definition is indexed
|
|
58
|
-
* under the bare name; the mode is kept for override analysis.
|
|
59
|
-
*/
|
|
60
|
-
entryMode?: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** One structured PdxDoc tag line (§E1). Unknown tags render as prose, not stored here. */
|
|
64
|
-
export interface DocTag {
|
|
65
|
-
/** Tag name without the leading `@` (lowercased). */
|
|
66
|
-
tag: string;
|
|
67
|
-
/** Text after the tag word. */
|
|
68
|
-
text: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** One usage site of a name, extracted schema-driven from mod files. */
|
|
72
|
-
export interface Reference {
|
|
73
|
-
name: string;
|
|
74
|
-
/** Candidate definition kinds this usage may refer to. */
|
|
75
|
-
kinds: DefKind[];
|
|
76
|
-
/** Absolute path. */
|
|
77
|
-
file: string;
|
|
78
|
-
/** 0-based line. */
|
|
79
|
-
line: number;
|
|
80
|
-
/** Character range of the name on the line (prefix like `scope:` excluded). */
|
|
81
|
-
startChar: number;
|
|
82
|
-
endChar: number;
|
|
83
|
-
/** Key-position call site (`my_effect = yes`): shown by find-references and
|
|
84
|
-
* rename, excluded from the usage-count completion ranking signal (§C2). */
|
|
85
|
-
call?: boolean;
|
|
86
|
-
/** Call sites only: enclosing key chain below the top-level definition
|
|
87
|
-
* (dotted, outermost first) — input for call-site scope aggregation, which
|
|
88
|
-
* types un-@scope'd scripted effects/triggers from where they are called. */
|
|
89
|
-
chain?: string;
|
|
90
|
-
}
|
|
1
|
+
/** Shared data model. Keep this module free of `vscode` imports: it is used by unit-tested code. */
|
|
2
|
+
|
|
3
|
+
export interface IndexStats {
|
|
4
|
+
total: number;
|
|
5
|
+
files: number;
|
|
6
|
+
byKind: Record<string, number>;
|
|
7
|
+
bySource: Record<string, number>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type TokenKind = "trigger" | "effect" | "event_target" | "modifier";
|
|
11
|
+
|
|
12
|
+
/** One engine token parsed from a script_docs log file. */
|
|
13
|
+
export interface TokenData {
|
|
14
|
+
name: string;
|
|
15
|
+
kind: TokenKind;
|
|
16
|
+
/** Description text from the log; may be empty. */
|
|
17
|
+
doc: string;
|
|
18
|
+
/** Supported scopes as raw strings, display-only in v1. */
|
|
19
|
+
scopes: string[];
|
|
20
|
+
/** Extra metadata lines (targets, traits, categories...), display-only. */
|
|
21
|
+
traits?: string;
|
|
22
|
+
/** A syntax/usage example block (`add_hook = { type = X ... }`), preserved
|
|
23
|
+
* verbatim from a `usage:` section, an inline `name = …` line, or the wiki. */
|
|
24
|
+
usage?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Definition kinds are open strings driven by the schema table
|
|
29
|
+
* (packages/server/src/schema): "scripted_effect", "trait", "decision", ...
|
|
30
|
+
*/
|
|
31
|
+
export type DefKind = string;
|
|
32
|
+
|
|
33
|
+
/** Where a definition comes from; mod shadows parent shadows vanilla. */
|
|
34
|
+
export type DefSource = "vanilla" | "parent" | "mod";
|
|
35
|
+
|
|
36
|
+
/** One user-defined or vanilla definition found by the indexer. */
|
|
37
|
+
export interface Definition {
|
|
38
|
+
name: string;
|
|
39
|
+
kind: DefKind;
|
|
40
|
+
/** Absolute path. */
|
|
41
|
+
file: string;
|
|
42
|
+
/** 0-based line number (VS Code convention). */
|
|
43
|
+
line: number;
|
|
44
|
+
source: DefSource;
|
|
45
|
+
/** For loc_key: the localized text (truncated for memory; the edit flow re-reads the yml). */
|
|
46
|
+
value?: string;
|
|
47
|
+
/** Enclosing definition, when meaningful (e.g. the event a save_scope_as sits in). */
|
|
48
|
+
container?: string;
|
|
49
|
+
/** For scripted effects/triggers: $PARAM$ names in declaration order (signature help). */
|
|
50
|
+
params?: string[];
|
|
51
|
+
/** PdxDoc prose from a leading `#` comment block (§E); capped for memory. */
|
|
52
|
+
doc?: string;
|
|
53
|
+
/** PdxDoc structured tags (@scope, @param, @saves, @returns, @example, @deprecated, …). */
|
|
54
|
+
tags?: DocTag[];
|
|
55
|
+
/**
|
|
56
|
+
* Database entry mode stripped from the declaration key (`REPLACE:name`),
|
|
57
|
+
* for games whose profile declares entryModes. The definition is indexed
|
|
58
|
+
* under the bare name; the mode is kept for override analysis.
|
|
59
|
+
*/
|
|
60
|
+
entryMode?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One structured PdxDoc tag line (§E1). Unknown tags render as prose, not stored here. */
|
|
64
|
+
export interface DocTag {
|
|
65
|
+
/** Tag name without the leading `@` (lowercased). */
|
|
66
|
+
tag: string;
|
|
67
|
+
/** Text after the tag word. */
|
|
68
|
+
text: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One usage site of a name, extracted schema-driven from mod files. */
|
|
72
|
+
export interface Reference {
|
|
73
|
+
name: string;
|
|
74
|
+
/** Candidate definition kinds this usage may refer to. */
|
|
75
|
+
kinds: DefKind[];
|
|
76
|
+
/** Absolute path. */
|
|
77
|
+
file: string;
|
|
78
|
+
/** 0-based line. */
|
|
79
|
+
line: number;
|
|
80
|
+
/** Character range of the name on the line (prefix like `scope:` excluded). */
|
|
81
|
+
startChar: number;
|
|
82
|
+
endChar: number;
|
|
83
|
+
/** Key-position call site (`my_effect = yes`): shown by find-references and
|
|
84
|
+
* rename, excluded from the usage-count completion ranking signal (§C2). */
|
|
85
|
+
call?: boolean;
|
|
86
|
+
/** Call sites only: enclosing key chain below the top-level definition
|
|
87
|
+
* (dotted, outermost first) — input for call-site scope aggregation, which
|
|
88
|
+
* types un-@scope'd scripted effects/triggers from where they are called. */
|
|
89
|
+
chain?: string;
|
|
90
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mod's local Workshop record: `<mod>/<configDir>/workshop.json`. It holds
|
|
3
|
+
* what the game's own descriptor has no field for - the item's description and
|
|
4
|
+
* its per-language translations (title + description shown to Workshop
|
|
5
|
+
* visitors browsing Steam in that language), plus the published id for the
|
|
6
|
+
* `.metadata` games (launcher-`.mod` games keep the id in `remote_file_id`).
|
|
7
|
+
*
|
|
8
|
+
* The local file is the canonical copy once the user manages the item from the
|
|
9
|
+
* toolkit: uploads read from here, and "fetch" pulls the live values down into
|
|
10
|
+
* it. Reads and writes are merge-preserving: keys this version does not know
|
|
11
|
+
* survive a round trip.
|
|
12
|
+
*
|
|
13
|
+
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
|
|
18
|
+
/** Title/description pair of one Workshop language. Absent field = not translated. */
|
|
19
|
+
export interface WorkshopTranslation {
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The fields of `workshop.json` this toolkit reads or writes. */
|
|
25
|
+
export interface WorkshopMeta {
|
|
26
|
+
/** Workshop item id (decimal string), for the games whose descriptor has no field for it. */
|
|
27
|
+
publishedFileId?: string;
|
|
28
|
+
/** The item's description in the default language, BBCode as Steam renders it. */
|
|
29
|
+
description?: string;
|
|
30
|
+
/** Keyed by Steam API language code (`german`, `schinese`, ...), never the default language. */
|
|
31
|
+
translations?: Record<string, WorkshopTranslation>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Mod-root-relative path of the record, forward slashes. */
|
|
35
|
+
export function workshopMetaRelPath(configDirName: string): string {
|
|
36
|
+
return `${configDirName}/workshop.json`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The parsed `<dir>/<configDir>/workshop.json`, or null when absent/unreadable. */
|
|
40
|
+
export function readWorkshopMeta(dir: string, configDirName: string): WorkshopMeta | null {
|
|
41
|
+
try {
|
|
42
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, configDirName, "workshop.json"), "utf8")) as Record<
|
|
43
|
+
string,
|
|
44
|
+
unknown
|
|
45
|
+
>;
|
|
46
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
47
|
+
return raw as WorkshopMeta;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Merge `patch` into the record and write it back. Unknown keys of the file
|
|
55
|
+
* survive; a patch key set to `undefined` is left as it was. `translations`
|
|
56
|
+
* replaces as a whole (the caller edits the full map).
|
|
57
|
+
*/
|
|
58
|
+
export function upsertWorkshopMeta(dir: string, configDirName: string, patch: WorkshopMeta): void {
|
|
59
|
+
const file = path.join(dir, configDirName, "workshop.json");
|
|
60
|
+
const current = (readWorkshopMeta(dir, configDirName) ?? {}) as Record<string, unknown>;
|
|
61
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
62
|
+
if (value !== undefined) current[key] = value;
|
|
63
|
+
}
|
|
64
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
65
|
+
fs.writeFileSync(file, JSON.stringify(current, null, 2) + "\n", "utf8");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The languages the Workshop accepts item text in: Steam's API language codes
|
|
70
|
+
* with their English names, in Steam's documented order.
|
|
71
|
+
* https://partner.steamgames.com/doc/store/localization/languages
|
|
72
|
+
*/
|
|
73
|
+
export const STEAM_LANGUAGES: readonly { api: string; label: string }[] = [
|
|
74
|
+
{ api: "arabic", label: "Arabic" },
|
|
75
|
+
{ api: "bulgarian", label: "Bulgarian" },
|
|
76
|
+
{ api: "schinese", label: "Chinese (Simplified)" },
|
|
77
|
+
{ api: "tchinese", label: "Chinese (Traditional)" },
|
|
78
|
+
{ api: "czech", label: "Czech" },
|
|
79
|
+
{ api: "danish", label: "Danish" },
|
|
80
|
+
{ api: "dutch", label: "Dutch" },
|
|
81
|
+
{ api: "english", label: "English" },
|
|
82
|
+
{ api: "finnish", label: "Finnish" },
|
|
83
|
+
{ api: "french", label: "French" },
|
|
84
|
+
{ api: "german", label: "German" },
|
|
85
|
+
{ api: "greek", label: "Greek" },
|
|
86
|
+
{ api: "hungarian", label: "Hungarian" },
|
|
87
|
+
{ api: "indonesian", label: "Indonesian" },
|
|
88
|
+
{ api: "italian", label: "Italian" },
|
|
89
|
+
{ api: "japanese", label: "Japanese" },
|
|
90
|
+
{ api: "koreana", label: "Korean" },
|
|
91
|
+
{ api: "norwegian", label: "Norwegian" },
|
|
92
|
+
{ api: "polish", label: "Polish" },
|
|
93
|
+
{ api: "portuguese", label: "Portuguese" },
|
|
94
|
+
{ api: "brazilian", label: "Portuguese (Brazil)" },
|
|
95
|
+
{ api: "romanian", label: "Romanian" },
|
|
96
|
+
{ api: "russian", label: "Russian" },
|
|
97
|
+
{ api: "spanish", label: "Spanish (Spain)" },
|
|
98
|
+
{ api: "latam", label: "Spanish (Latin America)" },
|
|
99
|
+
{ api: "swedish", label: "Swedish" },
|
|
100
|
+
{ api: "thai", label: "Thai" },
|
|
101
|
+
{ api: "turkish", label: "Turkish" },
|
|
102
|
+
{ api: "ukrainian", label: "Ukrainian" },
|
|
103
|
+
{ api: "vietnamese", label: "Vietnamese" },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/** English name of a Steam API language code; the code itself when unknown. */
|
|
107
|
+
export function steamLanguageLabel(api: string): string {
|
|
108
|
+
return STEAM_LANGUAGES.find((l) => l.api === api)?.label ?? api;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Steam API language code for a Paradox localization folder language
|
|
113
|
+
* (`translationCore.ts` LOC_LANGUAGES), or null when Steam has no counterpart.
|
|
114
|
+
* The two vocabularies differ where Steam's codes predate its own store pages
|
|
115
|
+
* (`koreana`, `schinese`).
|
|
116
|
+
*/
|
|
117
|
+
export function steamLanguageForLoc(locLanguage: string): string | null {
|
|
118
|
+
const map: Record<string, string> = {
|
|
119
|
+
english: "english",
|
|
120
|
+
french: "french",
|
|
121
|
+
german: "german",
|
|
122
|
+
spanish: "spanish",
|
|
123
|
+
russian: "russian",
|
|
124
|
+
korean: "koreana",
|
|
125
|
+
simp_chinese: "schinese",
|
|
126
|
+
japanese: "japanese",
|
|
127
|
+
polish: "polish",
|
|
128
|
+
braz_por: "brazilian",
|
|
129
|
+
turkish: "turkish",
|
|
130
|
+
};
|
|
131
|
+
return map[locLanguage] ?? null;
|
|
132
|
+
}
|