@px-lsp/protocol 0.1.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 -0
- package/README.md +25 -0
- package/dist/arrays.d.ts +13 -0
- package/dist/arrays.js +19 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +10 -0
- package/dist/descriptorMetadata.d.ts +51 -0
- package/dist/descriptorMetadata.js +98 -0
- package/dist/descriptorMod.d.ts +66 -0
- package/dist/descriptorMod.js +335 -0
- package/dist/errorLogParser.d.ts +33 -0
- package/dist/errorLogParser.js +125 -0
- package/dist/fsWalk.d.ts +20 -0
- package/dist/fsWalk.js +159 -0
- package/dist/locProperties.d.ts +13 -0
- package/dist/locProperties.js +46 -0
- package/dist/locRefs.d.ts +11 -0
- package/dist/locRefs.js +31 -0
- package/dist/modName.d.ts +6 -0
- package/dist/modName.js +53 -0
- package/dist/protocol.d.ts +1462 -0
- package/dist/protocol.js +201 -0
- package/dist/regex.d.ts +13 -0
- package/dist/regex.js +21 -0
- package/dist/suppression.d.ts +52 -0
- package/dist/suppression.js +173 -0
- package/dist/tigerParser.d.ts +28 -0
- package/dist/tigerParser.js +72 -0
- package/dist/translationCore.d.ts +26 -0
- package/dist/translationCore.js +162 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +3 -0
- package/package.json +39 -0
- package/src/arrays.ts +16 -0
- package/src/constants.ts +12 -0
- package/src/descriptorMetadata.ts +101 -0
- package/src/descriptorMod.ts +354 -0
- package/src/errorLogParser.ts +136 -0
- package/src/fsWalk.ts +126 -0
- package/src/locProperties.ts +43 -0
- package/src/locRefs.ts +38 -0
- package/src/modName.ts +18 -0
- package/src/protocol.ts +1459 -0
- package/src/regex.ts +19 -0
- package/src/suppression.ts +178 -0
- package/src/tigerParser.ts +79 -0
- package/src/translationCore.ts +140 -0
- package/src/types.ts +90 -0
|
@@ -0,0 +1,162 @@
|
|
|
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.LOC_LANGUAGES = void 0;
|
|
37
|
+
exports.detectLocFileLanguage = detectLocFileLanguage;
|
|
38
|
+
exports.retargetLocPath = retargetLocPath;
|
|
39
|
+
exports.buildTranslation = buildTranslation;
|
|
40
|
+
exports.mergeTranslation = mergeTranslation;
|
|
41
|
+
/**
|
|
42
|
+
* Pure logic for the translation scaffolding workflow: mirror the structure of
|
|
43
|
+
* an existing localization (usually english) into a new language so a
|
|
44
|
+
* translator only has to replace values.
|
|
45
|
+
*
|
|
46
|
+
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
47
|
+
*/
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
exports.LOC_LANGUAGES = [
|
|
50
|
+
"english",
|
|
51
|
+
"french",
|
|
52
|
+
"german",
|
|
53
|
+
"spanish",
|
|
54
|
+
"russian",
|
|
55
|
+
"korean",
|
|
56
|
+
"simp_chinese",
|
|
57
|
+
"japanese",
|
|
58
|
+
"polish",
|
|
59
|
+
];
|
|
60
|
+
const BOM = "";
|
|
61
|
+
const HEADER = /^(\s*)l_([a-z_]+):/m;
|
|
62
|
+
const ENTRY = /^\s*([A-Za-z0-9_.\-']+):\d*\s*"/;
|
|
63
|
+
/** Language of a loc file, from its `_l_<lang>.yml` suffix or a path segment. */
|
|
64
|
+
function detectLocFileLanguage(filePath) {
|
|
65
|
+
const m = /_l_([a-z_]+)\.ya?ml$/i.exec(filePath);
|
|
66
|
+
if (m)
|
|
67
|
+
return m[1].toLowerCase();
|
|
68
|
+
const segments = filePath.toLowerCase().split(/[\\/]/);
|
|
69
|
+
for (const lang of exports.LOC_LANGUAGES) {
|
|
70
|
+
if (segments.includes(lang))
|
|
71
|
+
return lang;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Where the translated counterpart of `srcFile` lives: language path segments
|
|
77
|
+
* and the `_l_<lang>` filename marker are retargeted. Returns null when the
|
|
78
|
+
* path carries no language marker at all.
|
|
79
|
+
*/
|
|
80
|
+
function retargetLocPath(srcFile, sourceLang, targetLang) {
|
|
81
|
+
const parts = srcFile.split(/([\\/])/); // keep separators
|
|
82
|
+
let changed = false;
|
|
83
|
+
const out = parts.map((p) => {
|
|
84
|
+
if (p.toLowerCase() === sourceLang) {
|
|
85
|
+
changed = true;
|
|
86
|
+
return targetLang;
|
|
87
|
+
}
|
|
88
|
+
return p;
|
|
89
|
+
});
|
|
90
|
+
let result = out.join("");
|
|
91
|
+
const marker = new RegExp(`_l_${sourceLang}(\\.ya?ml)$`, "i");
|
|
92
|
+
if (marker.test(path.basename(result))) {
|
|
93
|
+
result = result.replace(marker, `_l_${targetLang}$1`);
|
|
94
|
+
changed = true;
|
|
95
|
+
}
|
|
96
|
+
return changed ? result : null;
|
|
97
|
+
}
|
|
98
|
+
// Greedy `(.*)` closes the value at the LAST quote before any trailing
|
|
99
|
+
// comment, matching the game's parsing of inner quotes (`""speech""`).
|
|
100
|
+
const ENTRY_LINE = /^(\s*[A-Za-z0-9_.\-']+:\d*\s*)"(.*)"\s*(#.*)?$/;
|
|
101
|
+
/** Blank an entry's value, keeping the source text visible as a comment. */
|
|
102
|
+
function blankEntry(line, sourceLang) {
|
|
103
|
+
const m = ENTRY_LINE.exec(line);
|
|
104
|
+
if (!m || m[2] === "")
|
|
105
|
+
return line;
|
|
106
|
+
// The game reads the value up to the last quote on the LINE, so any quote
|
|
107
|
+
// echoed into the comment would leak back into the value; downgrade to '.
|
|
108
|
+
const comment = `# ${sourceLang}: ${m[2]}${m[3] ? ` ${m[3]}` : ""}`.replace(/"/g, "'");
|
|
109
|
+
return `${m[1]}"" ${comment}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A translation skeleton: the source file's structure (comments and blank
|
|
113
|
+
* lines preserved — they are context for the translator) with the language
|
|
114
|
+
* header switched and every value BLANKED; the source text stays visible as
|
|
115
|
+
* an inline `# english: …` comment so the translator sees it right there
|
|
116
|
+
* without it leaking into the game as a fake translation.
|
|
117
|
+
*/
|
|
118
|
+
function buildTranslation(sourceContent, targetLang, sourceLang = "english") {
|
|
119
|
+
const hadBom = sourceContent.startsWith(BOM);
|
|
120
|
+
let body = hadBom ? sourceContent.slice(1) : sourceContent;
|
|
121
|
+
if (HEADER.test(body)) {
|
|
122
|
+
body = body.replace(HEADER, `$1l_${targetLang}:`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
body = `l_${targetLang}:\n` + body;
|
|
126
|
+
}
|
|
127
|
+
body = body
|
|
128
|
+
.split(/\r?\n/)
|
|
129
|
+
.map((l) => blankEntry(l, sourceLang))
|
|
130
|
+
.join("\n");
|
|
131
|
+
return BOM + body;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Add entries that exist in the source but not yet in the target, appended at
|
|
135
|
+
* the end under a marker comment. Existing target lines are never touched.
|
|
136
|
+
*/
|
|
137
|
+
function mergeTranslation(targetContent, sourceContent, sourceLang) {
|
|
138
|
+
const hadBom = targetContent.startsWith(BOM);
|
|
139
|
+
const target = hadBom ? targetContent.slice(1) : targetContent;
|
|
140
|
+
const eol = target.includes("\r\n") ? "\r\n" : "\n";
|
|
141
|
+
const existing = new Set();
|
|
142
|
+
for (const line of target.split(/\r?\n/)) {
|
|
143
|
+
const m = ENTRY.exec(line);
|
|
144
|
+
if (m)
|
|
145
|
+
existing.add(m[1]);
|
|
146
|
+
}
|
|
147
|
+
const missing = [];
|
|
148
|
+
for (const line of sourceContent.replace(/^/, "").split(/\r?\n/)) {
|
|
149
|
+
const m = ENTRY.exec(line);
|
|
150
|
+
if (m && !existing.has(m[1])) {
|
|
151
|
+
missing.push(blankEntry(line.replace(/\r$/, ""), sourceLang));
|
|
152
|
+
existing.add(m[1]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (missing.length === 0)
|
|
156
|
+
return { content: targetContent, added: 0 };
|
|
157
|
+
const lines = target.split(/\r?\n/);
|
|
158
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "")
|
|
159
|
+
lines.pop();
|
|
160
|
+
lines.push("", ` # --- entries missing from this language; ${sourceLang} text in the comments ---`, ...missing, "");
|
|
161
|
+
return { content: (hadBom ? BOM : "") + lines.join(eol), added: missing.length };
|
|
162
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Shared data model. Keep this module free of `vscode` imports: it is used by unit-tested code. */
|
|
2
|
+
export interface IndexStats {
|
|
3
|
+
total: number;
|
|
4
|
+
files: number;
|
|
5
|
+
byKind: Record<string, number>;
|
|
6
|
+
bySource: Record<string, number>;
|
|
7
|
+
}
|
|
8
|
+
export type TokenKind = "trigger" | "effect" | "event_target" | "modifier";
|
|
9
|
+
/** One engine token parsed from a script_docs log file. */
|
|
10
|
+
export interface TokenData {
|
|
11
|
+
name: string;
|
|
12
|
+
kind: TokenKind;
|
|
13
|
+
/** Description text from the log; may be empty. */
|
|
14
|
+
doc: string;
|
|
15
|
+
/** Supported scopes as raw strings, display-only in v1. */
|
|
16
|
+
scopes: string[];
|
|
17
|
+
/** Extra metadata lines (targets, traits, categories...), display-only. */
|
|
18
|
+
traits?: string;
|
|
19
|
+
/** A syntax/usage example block (`add_hook = { type = X ... }`), preserved
|
|
20
|
+
* verbatim from a `usage:` section, an inline `name = …` line, or the wiki. */
|
|
21
|
+
usage?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Definition kinds are open strings driven by the schema table
|
|
25
|
+
* (packages/server/src/schema): "scripted_effect", "trait", "decision", ...
|
|
26
|
+
*/
|
|
27
|
+
export type DefKind = string;
|
|
28
|
+
/** Where a definition comes from; mod shadows parent shadows vanilla. */
|
|
29
|
+
export type DefSource = "vanilla" | "parent" | "mod";
|
|
30
|
+
/** One user-defined or vanilla definition found by the indexer. */
|
|
31
|
+
export interface Definition {
|
|
32
|
+
name: string;
|
|
33
|
+
kind: DefKind;
|
|
34
|
+
/** Absolute path. */
|
|
35
|
+
file: string;
|
|
36
|
+
/** 0-based line number (VS Code convention). */
|
|
37
|
+
line: number;
|
|
38
|
+
source: DefSource;
|
|
39
|
+
/** For loc_key: the localized text (truncated for memory; the edit flow re-reads the yml). */
|
|
40
|
+
value?: string;
|
|
41
|
+
/** Enclosing definition, when meaningful (e.g. the event a save_scope_as sits in). */
|
|
42
|
+
container?: string;
|
|
43
|
+
/** For scripted effects/triggers: $PARAM$ names in declaration order (signature help). */
|
|
44
|
+
params?: string[];
|
|
45
|
+
/** PdxDoc prose from a leading `#` comment block (§E); capped for memory. */
|
|
46
|
+
doc?: string;
|
|
47
|
+
/** PdxDoc structured tags (@scope, @param, @saves, @returns, @example, @deprecated, …). */
|
|
48
|
+
tags?: DocTag[];
|
|
49
|
+
/**
|
|
50
|
+
* Database entry mode stripped from the declaration key (`REPLACE:name`),
|
|
51
|
+
* for games whose profile declares entryModes. The definition is indexed
|
|
52
|
+
* under the bare name; the mode is kept for override analysis.
|
|
53
|
+
*/
|
|
54
|
+
entryMode?: string;
|
|
55
|
+
}
|
|
56
|
+
/** One structured PdxDoc tag line (§E1). Unknown tags render as prose, not stored here. */
|
|
57
|
+
export interface DocTag {
|
|
58
|
+
/** Tag name without the leading `@` (lowercased). */
|
|
59
|
+
tag: string;
|
|
60
|
+
/** Text after the tag word. */
|
|
61
|
+
text: string;
|
|
62
|
+
}
|
|
63
|
+
/** One usage site of a name, extracted schema-driven from mod files. */
|
|
64
|
+
export interface Reference {
|
|
65
|
+
name: string;
|
|
66
|
+
/** Candidate definition kinds this usage may refer to. */
|
|
67
|
+
kinds: DefKind[];
|
|
68
|
+
/** Absolute path. */
|
|
69
|
+
file: string;
|
|
70
|
+
/** 0-based line. */
|
|
71
|
+
line: number;
|
|
72
|
+
/** Character range of the name on the line (prefix like `scope:` excluded). */
|
|
73
|
+
startChar: number;
|
|
74
|
+
endChar: number;
|
|
75
|
+
/** Key-position call site (`my_effect = yes`): shown by find-references and
|
|
76
|
+
* rename, excluded from the usage-count completion ranking signal (§C2). */
|
|
77
|
+
call?: boolean;
|
|
78
|
+
/** Call sites only: enclosing key chain below the top-level definition
|
|
79
|
+
* (dotted, outermost first) — input for call-site scope aggregation, which
|
|
80
|
+
* types un-@scope'd scripted effects/triggers from where they are called. */
|
|
81
|
+
chain?: string;
|
|
82
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@px-lsp/protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "GPL-3.0-or-later",
|
|
5
|
+
"description": "Wire contract (custom LSP requests/notifications, settings types) and shared helpers for the px-lsp language server and its clients.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"lsp",
|
|
8
|
+
"paradox",
|
|
9
|
+
"modding"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/JDeffner/paradox-modding-toolkit.git",
|
|
14
|
+
"directory": "packages/protocol"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/protocol.d.ts",
|
|
25
|
+
"default": "./dist/protocol.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json",
|
|
28
|
+
"./*": {
|
|
29
|
+
"types": "./dist/*.d.ts",
|
|
30
|
+
"default": "./dist/*.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.build.json"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/arrays.ts
ADDED
|
@@ -0,0 +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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +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
|
+
];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader and writer for the newer Paradox mod descriptor convention:
|
|
3
|
+
* `<mod>/.metadata/metadata.json` (newer titles) instead of the launcher
|
|
4
|
+
* `.mod` file. Fail-soft on read: any read/parse problem yields null.
|
|
5
|
+
*
|
|
6
|
+
* The field set is copied from three real workshop mods (2026-08-12: name, id,
|
|
7
|
+
* version, supported_game_version, tags, relationships, game_custom_data;
|
|
8
|
+
* `game_id` appears in one of the three and is left out here because the other
|
|
9
|
+
* two load without it). The relationship shape is the one the Community Mod
|
|
10
|
+
* Framework documents for the mods that depend on it.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
|
|
15
|
+
/** Mod-root-relative path of the descriptor, forward slashes. */
|
|
16
|
+
export const METADATA_REL_PATH = ".metadata/metadata.json";
|
|
17
|
+
|
|
18
|
+
/** One entry of `relationships`: a link to another mod. */
|
|
19
|
+
export interface MetadataRelationship {
|
|
20
|
+
/** "dependency", "incompatible_with", "load_before", "load_after". */
|
|
21
|
+
rel_type: string;
|
|
22
|
+
/** The other mod's `id` field (NOT its Workshop number). */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Shown when the other mod is not on disk. */
|
|
25
|
+
display_name?: string;
|
|
26
|
+
/** Only "mod" is supported by the launcher today. */
|
|
27
|
+
resource_type: string;
|
|
28
|
+
/** Version of the other mod, `*` for any. */
|
|
29
|
+
version?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The fields of a mod's metadata.json this toolkit reads or writes. */
|
|
33
|
+
export interface ModMetadata {
|
|
34
|
+
name?: string;
|
|
35
|
+
id?: string;
|
|
36
|
+
version?: string;
|
|
37
|
+
supported_game_version?: string;
|
|
38
|
+
short_description?: string;
|
|
39
|
+
tags?: string[];
|
|
40
|
+
relationships?: MetadataRelationship[];
|
|
41
|
+
game_custom_data?: { multiplayer_synchronized?: boolean; replace_paths?: string[] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The parsed `<dir>/.metadata/metadata.json`, or null when absent/unreadable. */
|
|
45
|
+
export function readMetadata(dir: string): ModMetadata | null {
|
|
46
|
+
try {
|
|
47
|
+
const file = path.join(dir, ".metadata", "metadata.json");
|
|
48
|
+
if (!fs.existsSync(file)) return null;
|
|
49
|
+
return JSON.parse(fs.readFileSync(file, "utf8")) as ModMetadata;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The mod's display name from `<dir>/.metadata/metadata.json`, or null. */
|
|
56
|
+
export function readMetadataName(dir: string): string | null {
|
|
57
|
+
const name = readMetadata(dir)?.name;
|
|
58
|
+
return typeof name === "string" && name.trim() !== "" ? name : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when `dir` carries a metadata-style descriptor. */
|
|
62
|
+
export function hasMetadataDescriptor(dir: string): boolean {
|
|
63
|
+
try {
|
|
64
|
+
return fs.existsSync(path.join(dir, ".metadata", "metadata.json"));
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MetadataScaffold {
|
|
71
|
+
name: string;
|
|
72
|
+
/** Stable identifier other mods point their relationships at. */
|
|
73
|
+
id: string;
|
|
74
|
+
/** The mod's own version, not the game's. */
|
|
75
|
+
version?: string;
|
|
76
|
+
/** Game version the mod is for, `*` when unknown. */
|
|
77
|
+
supportedGameVersion: string;
|
|
78
|
+
shortDescription?: string;
|
|
79
|
+
tags?: string[];
|
|
80
|
+
relationships?: MetadataRelationship[];
|
|
81
|
+
/** Vanilla folders the mod unloads wholesale (total conversions). */
|
|
82
|
+
replacePaths?: string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A launcher-correct starter metadata.json, in the corpus's field order. */
|
|
86
|
+
export function scaffoldMetadata(opts: MetadataScaffold): string {
|
|
87
|
+
const body: ModMetadata = {
|
|
88
|
+
name: opts.name,
|
|
89
|
+
id: opts.id,
|
|
90
|
+
version: opts.version ?? "0.1.0",
|
|
91
|
+
supported_game_version: opts.supportedGameVersion,
|
|
92
|
+
...(opts.shortDescription ? { short_description: opts.shortDescription } : {}),
|
|
93
|
+
tags: opts.tags ?? [],
|
|
94
|
+
relationships: opts.relationships ?? [],
|
|
95
|
+
game_custom_data: {
|
|
96
|
+
multiplayer_synchronized: true,
|
|
97
|
+
...(opts.replacePaths && opts.replacePaths.length > 0 ? { replace_paths: opts.replacePaths } : {}),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
return JSON.stringify(body, null, 2) + "\n";
|
|
101
|
+
}
|