modwright 0.1.1 → 0.1.2
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/README.md +82 -13
- package/bridges/bg3-se/README.md +15 -0
- package/bridges/cp2077-cet/README.md +14 -0
- package/dist/core/bridge/pending.js +8 -1
- package/dist/core/bridge/stage.js +2 -2
- package/dist/core/build/run.js +14 -0
- package/dist/core/build/steps/core.js +4 -4
- package/dist/core/deploy/plan.js +68 -1
- package/dist/core/deploy/reload.js +5 -3
- package/dist/core/ledger/hooks.js +22 -5
- package/dist/core/logs/scan.js +22 -6
- package/dist/core/project/index.js +1 -0
- package/dist/core/project/load.js +3 -26
- package/dist/core/project/outside-root.js +36 -0
- package/dist/core/project/schema.js +38 -1
- package/dist/core/safety/backup.js +2 -2
- package/dist/core/safety/execute.js +12 -4
- package/dist/core/safety/restore.js +62 -0
- package/dist/core/text/converters/index.js +1 -1
- package/dist/core/text/toolchain.js +2 -2
- package/dist/core/toolchain/locate.js +13 -1
- package/dist/core/toolchain/run.js +6 -4
- package/dist/core/toolchain/types.js +1 -1
- package/dist/core/trust.js +105 -0
- package/dist/index.js +33 -7
- package/dist/server.js +110 -21
- package/dist/surfaces/cyberpunk2077/compat.js +8 -4
- package/dist/surfaces/cyberpunk2077/logs.js +4 -3
- package/dist/surfaces/cyberpunk2077/surface.js +2 -1
- package/dist/surfaces/cyberpunk2077/validators/cr2w.js +9 -3
- package/dist/surfaces/eldenring/loaders.js +7 -1
- package/package.json +1 -1
- package/scaffolds/baldursgate3/new-project.yaml +4 -1
- package/scaffolds/cyberpunk2077/new-project.yaml +4 -1
package/dist/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { describeProject, findProjectFile, loadProject } from "./core/project/in
|
|
|
7
7
|
import { userDataDir } from "./core/home.js";
|
|
8
8
|
import { applyDataFetch, applyManagedInstall, checkToolchain, featureReadiness, locateTool, planDataFetch, planDataMemory, planManagedInstall, planToolMemory, readinessSummary, TOOL_SPECS, toolsToLocate } from "./core/toolchain/index.js";
|
|
9
9
|
import { loadUserConfig } from "./core/userconfig.js";
|
|
10
|
+
import { describeNeeds, isProjectTrusted, planTrust, projectTrustNeeds, trustRefusal } from "./core/trust.js";
|
|
10
11
|
import { runCompat } from "./core/compat/check.js";
|
|
11
12
|
import { SignatureRegistry, triageLogs } from "./core/logs/index.js";
|
|
12
13
|
import { buildContext, createValidatorRegistry, runValidators } from "./core/validate/index.js";
|
|
@@ -16,7 +17,6 @@ import { armBridge, bridgeCandidateDirs, bridgeStatus, cetLogCandidates, listPen
|
|
|
16
17
|
import { runTestPlan } from "./core/testplan/index.js";
|
|
17
18
|
import { applyBuildLedgerHook, applyClaimTransition, applyDecisionRecord, applyValidateLedgerHook, checkLedger, claimsFilePath, listClaims, loadClaims, loadDecisions, saveClaims, saveDecisions, showClaim, } from "./core/ledger/index.js";
|
|
18
19
|
import { IndexStore, defaultCacheDir, findLegacyIndex, indexFile, legacyIndexHint, loadKnowledge, resolveIndexFile, searchFacts, } from "./core/knowledge/index.js";
|
|
19
|
-
import { pathExists as fileExists } from "./core/fsutil.js";
|
|
20
20
|
import { executePlan, listBackups, restoreBackup } from "./core/safety/index.js";
|
|
21
21
|
import { readTextIfExists } from "./core/fsutil.js";
|
|
22
22
|
import * as path from "node:path";
|
|
@@ -368,9 +368,11 @@ server.registerTool("check_toolchain", {
|
|
|
368
368
|
"with data=<id> fetches a data folder ModWright can fetch (Stardew Valley's wiki pages) and remembers it; " +
|
|
369
369
|
"action 'remember' records a tool's path in the " +
|
|
370
370
|
"per-user config so every session finds it (no environment variable, no restart); " +
|
|
371
|
-
"'forget' drops it.
|
|
371
|
+
"'forget' drops it. 'trust' records a project root (projectPath) as allowed to run its own toolchain overrides and " +
|
|
372
|
+
"exec build steps; until then an applied build, convert, template extract or index build that would run them is " +
|
|
373
|
+
"refused. 'untrust' removes it. All of these are dry-run unless mode is 'apply'.",
|
|
372
374
|
inputSchema: {
|
|
373
|
-
action: z.enum(["check", "remember", "forget", "install"]).default("check").describe("check (default) reports; remember/forget change the per-user config; install fetches a pinned tool into ModWright's tools folder (dry-run shows the file, source, size and sha256 first), or with data a fetchable data folder (dry-run lists every URL first)."),
|
|
375
|
+
action: z.enum(["check", "remember", "forget", "install", "trust", "untrust"]).default("check").describe("check (default) reports; remember/forget change the per-user config; trust/untrust change the trusted-projects list for projectPath; install fetches a pinned tool into ModWright's tools folder (dry-run shows the file, source, size and sha256 first), or with data a fetchable data folder (dry-run lists every URL first)."),
|
|
374
376
|
game: gameArg.optional().describe("Restrict to tools used by this game (check only)."),
|
|
375
377
|
projectPath: z
|
|
376
378
|
.string()
|
|
@@ -380,9 +382,47 @@ server.registerTool("check_toolchain", {
|
|
|
380
382
|
toolPath: z.string().optional().describe("remember: absolute path to the executable, or the folder that directly holds it."),
|
|
381
383
|
data: z.string().optional().describe("remember/forget/install: a data folder id instead of a tool, e.g. 'valheim.jotunn' (a Jotunn checkout) or 'stardewvalley.wiki' (saved wiki pages); check_toolchain game=<id> lists the ones a game reads."),
|
|
382
384
|
dataPath: z.string().optional().describe("remember with data: absolute path to that folder. install with data: where to save it (default: ModWright's per-user data folder)."),
|
|
383
|
-
mode: z.enum(["dry-run", "apply"]).default("dry-run").describe("remember/forget: dry-run shows the config change; apply writes it (backing up the old file)."),
|
|
385
|
+
mode: z.enum(["dry-run", "apply"]).default("dry-run").describe("remember/forget/trust/untrust: dry-run shows the config change; apply writes it (backing up the old file)."),
|
|
384
386
|
},
|
|
385
387
|
}, async ({ action, game, projectPath, tool, toolPath, data, dataPath, mode }) => {
|
|
388
|
+
if (action === "trust" || action === "untrust") {
|
|
389
|
+
if (tool || toolPath || data || dataPath)
|
|
390
|
+
return fail(`${action} takes projectPath only.`);
|
|
391
|
+
if (!projectPath)
|
|
392
|
+
return fail(`${action} needs projectPath: the mod project's modwright.json or its folder.`);
|
|
393
|
+
let resolved;
|
|
394
|
+
try {
|
|
395
|
+
resolved = await resolveProject(projectPath);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
return fail(`Could not load project: ${error.message}`);
|
|
399
|
+
}
|
|
400
|
+
const planned = await planTrust(action, resolved.root, resolved.project.name);
|
|
401
|
+
if (!planned.ok)
|
|
402
|
+
return fail(planned.error);
|
|
403
|
+
const report = await executePlan(planned.plan, {
|
|
404
|
+
mode,
|
|
405
|
+
confidence: { level: "high", assumptions: ["The trusted-projects list is ModWright's own file; nothing in a game or a project is touched."] },
|
|
406
|
+
});
|
|
407
|
+
const needs = projectTrustNeeds(resolved.project);
|
|
408
|
+
return json({
|
|
409
|
+
action,
|
|
410
|
+
project: resolved.project.name,
|
|
411
|
+
root: planned.root,
|
|
412
|
+
mode,
|
|
413
|
+
applied: report.applied,
|
|
414
|
+
file: planned.file,
|
|
415
|
+
runs: { exec: needs.exec, toolchain: Object.fromEntries(needs.toolchain.map((id) => [id, resolved.project.toolchain?.[id]])) },
|
|
416
|
+
...(planned.previous ? { previous: planned.previous } : {}),
|
|
417
|
+
operations: report.operations,
|
|
418
|
+
...(report.backupDir ? { backupDir: report.backupDir } : {}),
|
|
419
|
+
note: report.applied
|
|
420
|
+
? action === "trust"
|
|
421
|
+
? `Trusted. Applied builds, converts, extracts and index builds may now run this project's exec steps and toolchain overrides.`
|
|
422
|
+
: `No longer trusted. Its exec steps and toolchain overrides are refused until it is trusted again.`
|
|
423
|
+
: `Dry run: nothing written. Read what it runs (runs) first; repeat with mode 'apply' to ${action} it.`,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
386
426
|
if ((action === "remember" || action === "forget") && data) {
|
|
387
427
|
if (tool)
|
|
388
428
|
return fail("Give either tool or data, not both.");
|
|
@@ -501,9 +541,12 @@ server.registerTool("check_toolchain", {
|
|
|
501
541
|
}
|
|
502
542
|
let projectOverrides;
|
|
503
543
|
let project;
|
|
544
|
+
let projectRoot;
|
|
504
545
|
if (projectPath) {
|
|
505
546
|
try {
|
|
506
|
-
|
|
547
|
+
const loaded = await loadProject(projectPath);
|
|
548
|
+
project = loaded.project;
|
|
549
|
+
projectRoot = loaded.root;
|
|
507
550
|
projectOverrides = project.toolchain;
|
|
508
551
|
}
|
|
509
552
|
catch (error) {
|
|
@@ -521,11 +564,13 @@ server.registerTool("check_toolchain", {
|
|
|
521
564
|
if (spec && !located.has(id))
|
|
522
565
|
located.set(id, await locateTool(spec, { projectOverrides, probe: "trusted", userTools: userConfig.config.tools }));
|
|
523
566
|
}
|
|
524
|
-
const indexBuilt = surface.index ? await
|
|
567
|
+
const indexBuilt = surface.index ? await pathExists(resolveIndexFile(surface.id).file) : undefined;
|
|
525
568
|
features = featureReadiness(surface, [...located.values()], { ...(project ? { project } : {}), ...(indexBuilt !== undefined ? { indexBuilt } : {}), userData: userConfig.config.data });
|
|
526
569
|
}
|
|
570
|
+
const trusted = projectRoot ? await isProjectTrusted(projectRoot) : undefined;
|
|
527
571
|
return json({
|
|
528
572
|
game: surfaceId,
|
|
573
|
+
...(projectRoot ? { project: { root: projectRoot, trusted, ...(trusted ? {} : { note: "Not trusted: an applied build, convert, extract or index build refuses to run its exec steps or toolchain overrides until check_toolchain action=trust." }) } } : {}),
|
|
529
574
|
...(features ? { summary: readinessSummary(features), features } : {}),
|
|
530
575
|
found: tools.filter((t) => t.found).length,
|
|
531
576
|
missing: tools.filter((t) => !t.found).map((t) => t.id),
|
|
@@ -686,7 +731,7 @@ function getStore(gameId, reopen = false) {
|
|
|
686
731
|
legacyStores.set(gameId, legacy);
|
|
687
732
|
else
|
|
688
733
|
legacyStores.delete(gameId);
|
|
689
|
-
pending =
|
|
734
|
+
pending = pathExists(file).then((exists) => (exists ? IndexStore.open(file) : undefined));
|
|
690
735
|
stores.set(gameId, pending);
|
|
691
736
|
}
|
|
692
737
|
return pending;
|
|
@@ -870,7 +915,12 @@ server.registerTool("build_index", {
|
|
|
870
915
|
const file = (await pathExists(projectPath)) && projectPath.toLowerCase().endsWith(".json") ? projectPath : await findProjectFile(projectPath);
|
|
871
916
|
if (!file)
|
|
872
917
|
return fail(`No modwright.json found at or above ${projectPath}.`);
|
|
873
|
-
|
|
918
|
+
const loaded = await loadProject(file);
|
|
919
|
+
toolOverrides = loaded.project.toolchain;
|
|
920
|
+
const used = Object.keys(toolOverrides ?? {}).filter((id) => TOOL_SPECS.find((s) => s.id === id)?.usedBy.includes(surface.id));
|
|
921
|
+
if (used.length > 0 && !(await isProjectTrusted(loaded.root))) {
|
|
922
|
+
return fail(trustRefusal(loaded.root, `its modwright.json overrides the tool path(s) for ${used.join(", ")}`));
|
|
923
|
+
}
|
|
874
924
|
}
|
|
875
925
|
catch (error) {
|
|
876
926
|
return fail(`Could not load project for tool overrides: ${error.message}`);
|
|
@@ -881,7 +931,7 @@ server.registerTool("build_index", {
|
|
|
881
931
|
if (legacy) {
|
|
882
932
|
return json({ game: surface.id, rebuilt: false, file: legacy.file, legacyIndex: legacy.file, note: `${legacyIndexHint(surface.id, legacy)} lookup already uses it. Pass force to build a fresh index in the new location instead.` });
|
|
883
933
|
}
|
|
884
|
-
if (!force && (await
|
|
934
|
+
if (!force && (await pathExists(file))) {
|
|
885
935
|
const existing = await surface.index.info(install);
|
|
886
936
|
if (!existing.stale)
|
|
887
937
|
return json({ ...existing, note: "Index already exists; pass force to rebuild." });
|
|
@@ -960,7 +1010,10 @@ server.registerTool("validate", {
|
|
|
960
1010
|
});
|
|
961
1011
|
const findings = includeInfo ? report.findings : report.findings.filter((finding) => finding.severity !== "info");
|
|
962
1012
|
const blockingFiles = report.findings.filter((f) => f.severity === "block" && !f.suppressedBy).map((f) => f.file);
|
|
963
|
-
const
|
|
1013
|
+
const narrowed = Boolean(ids?.length) || Boolean(groups?.length) || cheapOnly;
|
|
1014
|
+
const ledger = narrowed
|
|
1015
|
+
? { skipped: true, applied: [], refused: [], reason: "a run narrowed by rule, groups or cheapOnly never promotes a claim; run the full set" }
|
|
1016
|
+
: await applyValidateLedgerHook(resolved.root, blockingFiles, changedFiles, "validate");
|
|
964
1017
|
return json({
|
|
965
1018
|
...report,
|
|
966
1019
|
findings: findings.slice(0, 200),
|
|
@@ -1025,7 +1078,13 @@ server.registerTool("build", {
|
|
|
1025
1078
|
const ledger = mode === "apply" && !result.failed && result.manifestPath
|
|
1026
1079
|
? await applyBuildLedgerHook(resolved.root, result.manifestPath, "build")
|
|
1027
1080
|
: { skipped: true, applied: [], refused: [] };
|
|
1028
|
-
|
|
1081
|
+
let trust;
|
|
1082
|
+
if (mode === "dry-run") {
|
|
1083
|
+
const what = describeNeeds(projectTrustNeeds(resolved.project));
|
|
1084
|
+
if (what && !(await isProjectTrusted(resolved.root)))
|
|
1085
|
+
trust = { trusted: false, note: trustRefusal(resolved.root, what) };
|
|
1086
|
+
}
|
|
1087
|
+
return json({ ...result, summary, ledger, ...(trust ? { trust } : {}) });
|
|
1029
1088
|
});
|
|
1030
1089
|
const RELOAD_PATH_LIMIT = 200;
|
|
1031
1090
|
function trimReload(reload) {
|
|
@@ -1113,13 +1172,19 @@ server.registerTool("rollback", {
|
|
|
1113
1172
|
"purpose, entry count, how many of those entries are junction pointers rather than content, and total " +
|
|
1114
1173
|
"size. With a backupId it restores that backup: dry-run by default, and every file's sha256 is re-checked " +
|
|
1115
1174
|
"against the manifest immediately before it is written back. A junction is recreated as a junction, never " +
|
|
1116
|
-
"as a copy of what it pointed at."
|
|
1175
|
+
"as a copy of what it pointed at. A restore writes only under the project and the game's detected mod roots unless " +
|
|
1176
|
+
"allowOutsideRoots is set.",
|
|
1117
1177
|
inputSchema: {
|
|
1118
1178
|
path: z.string().optional().describe("modwright.json path or a directory to search from. Default: current directory."),
|
|
1119
1179
|
backupId: z.string().optional().describe("The stamp naming one backup directory under .modwright/backups (from this tool's own listing). Omit to list."),
|
|
1120
1180
|
mode: z.enum(["dry-run", "apply"]).default("dry-run").describe("dry-run reports what would be restored without writing a byte."),
|
|
1181
|
+
installPath: installPathArg,
|
|
1182
|
+
allowOutsideRoots: z
|
|
1183
|
+
.boolean()
|
|
1184
|
+
.default(false)
|
|
1185
|
+
.describe("Restore entries outside the project and the game's mod roots too. Off by default: a backup that names any other path is refused whole."),
|
|
1121
1186
|
},
|
|
1122
|
-
}, async ({ path: start, backupId, mode }) => {
|
|
1187
|
+
}, async ({ path: start, backupId, mode, installPath, allowOutsideRoots }) => {
|
|
1123
1188
|
const resolved = await projectOrFail(start);
|
|
1124
1189
|
if ("isError" in resolved)
|
|
1125
1190
|
return resolved;
|
|
@@ -1141,17 +1206,30 @@ server.registerTool("rollback", {
|
|
|
1141
1206
|
if (!(await pathExists(backupDir))) {
|
|
1142
1207
|
return fail(`No backup "${backupId}" under ${backupRoot}. Call rollback with no backupId to list what is there.`);
|
|
1143
1208
|
}
|
|
1209
|
+
let confineTo;
|
|
1210
|
+
let noInstall = false;
|
|
1211
|
+
if (!allowOutsideRoots) {
|
|
1212
|
+
const surface = getSurface(resolved.project.game);
|
|
1213
|
+
const install = surface ? await resolveInstall(surface, installPath).catch(() => undefined) : undefined;
|
|
1214
|
+
noInstall = !install;
|
|
1215
|
+
confineTo = [resolved.root, ...(surface && install ? surface.modRoots(install).map((r) => r.path) : [])];
|
|
1216
|
+
}
|
|
1144
1217
|
try {
|
|
1145
|
-
const report = await restoreBackup(backupDir, { mode });
|
|
1146
|
-
|
|
1218
|
+
const report = await restoreBackup(backupDir, { mode, ...(confineTo ? { confineTo } : {}) });
|
|
1219
|
+
const operations = report.operations.map(({ op, ...rest }) => ({
|
|
1220
|
+
...rest,
|
|
1221
|
+
op: "content" in op ? { kind: op.kind, path: op.path, bytes: rest.sizeBytes } : op,
|
|
1222
|
+
}));
|
|
1223
|
+
return json({ ...report, operations, backupId, backupDir });
|
|
1147
1224
|
}
|
|
1148
1225
|
catch (error) {
|
|
1149
|
-
|
|
1226
|
+
const hint = noInstall ? " The game's install was not detected, so only the project counts; pass installPath to add its mod roots." : "";
|
|
1227
|
+
return fail(`rollback failed: ${error.message}${hint}`);
|
|
1150
1228
|
}
|
|
1151
1229
|
});
|
|
1152
1230
|
server.registerTool("reload_plan", {
|
|
1153
1231
|
title: "Classify what a set of deployed paths needs to take effect",
|
|
1154
|
-
description: "Map deployed paths to a reload recommendation — hot, hot-degraded, save-reload, restart or unknown — " +
|
|
1232
|
+
description: "Map deployed paths to a reload recommendation — hot, hot-degraded, save-reload, restart or unknown (none when nothing was written) — " +
|
|
1155
1233
|
"using the project's own reload.rules first and then the per-surface table, and reduce them to the single " +
|
|
1156
1234
|
"most conservative answer with its caveats. An unmatched path is reported as unknown ('restart to be sure'), " +
|
|
1157
1235
|
"never promoted. ModWright never launches the game and never sends a reload command; this is a " +
|
|
@@ -1566,15 +1644,24 @@ server.registerTool("convert", {
|
|
|
1566
1644
|
},
|
|
1567
1645
|
}, async ({ conversionId, source, dest, mode, verifyRoundTrip, path: projectPath }) => {
|
|
1568
1646
|
let overrides;
|
|
1569
|
-
|
|
1647
|
+
let projectTrust;
|
|
1648
|
+
let backupRoot;
|
|
1649
|
+
const projectStart = projectPath ?? (path.isAbsolute(dest) ? (await findProjectFile(path.dirname(dest))) : undefined);
|
|
1650
|
+
if (projectStart) {
|
|
1570
1651
|
try {
|
|
1571
|
-
|
|
1652
|
+
const resolved = await resolveProject(projectStart);
|
|
1653
|
+
backupRoot = path.join(resolved.root, ".modwright", "backups");
|
|
1654
|
+
if (projectPath) {
|
|
1655
|
+
overrides = resolved.project.toolchain;
|
|
1656
|
+
if (overrides && Object.keys(overrides).length > 0)
|
|
1657
|
+
projectTrust = { root: resolved.root, trusted: await isProjectTrusted(resolved.root) };
|
|
1658
|
+
}
|
|
1572
1659
|
}
|
|
1573
1660
|
catch {
|
|
1574
1661
|
}
|
|
1575
1662
|
}
|
|
1576
1663
|
try {
|
|
1577
|
-
const report = await runConvert(conversionId, source, dest, { mode, verifyRoundTrip, locateTool: defaultTextLocateTool(overrides, mode === "apply" ? "all" : "none") });
|
|
1664
|
+
const report = await runConvert(conversionId, source, dest, { mode, verifyRoundTrip, locateTool: defaultTextLocateTool(overrides, mode === "apply" ? "all" : "none", projectTrust), ...(backupRoot ? { backupRoot } : {}) });
|
|
1578
1665
|
return json(report);
|
|
1579
1666
|
}
|
|
1580
1667
|
catch (error) {
|
|
@@ -1667,7 +1754,9 @@ server.registerTool("template", {
|
|
|
1667
1754
|
catch (error) {
|
|
1668
1755
|
return fail(`Could not load project: ${error.message}`);
|
|
1669
1756
|
}
|
|
1670
|
-
const
|
|
1757
|
+
const overrides = resolved.project.toolchain;
|
|
1758
|
+
const projectTrust = action === "extract" && overrides && Object.keys(overrides).length > 0 ? { root: resolved.root, trusted: await isProjectTrusted(resolved.root) } : undefined;
|
|
1759
|
+
const locateTool = defaultTextLocateTool(overrides, action === "extract" && mode === "apply" ? "all" : action === "extract" ? "none" : "trusted", projectTrust);
|
|
1671
1760
|
const templateGameVersion = async (surfaceId) => {
|
|
1672
1761
|
const gameSurface = getSurface(surfaceId);
|
|
1673
1762
|
if (!gameSurface)
|
|
@@ -210,11 +210,15 @@ const RULES = [
|
|
|
210
210
|
requires: [],
|
|
211
211
|
advisory: true,
|
|
212
212
|
severity: "info",
|
|
213
|
-
note: "RED4ext plugins are
|
|
214
|
-
"
|
|
213
|
+
note: "RED4ext plugins are tied to the game build they were compiled for. Before RED4ext 1.30.0 the " +
|
|
214
|
+
"loader itself refuses an unrecognized patch and no plugin loads; from 1.30.0 the loader runs on " +
|
|
215
|
+
"any build and skips each plugin pinned to another runtime (\"is incompatible with the current " +
|
|
216
|
+
"patch\" in red4ext.log), so a patch now breaks plugins one at a time. After a game patch, wait for " +
|
|
217
|
+
"RED4ext and its plugins to publish updated releases before assuming a mod is broken.",
|
|
215
218
|
source: "https://wiki.redmodding.org/cyberpunk-2077-modding/for-mod-users/user-guide-troubleshooting " +
|
|
216
|
-
"(\"prone to breaking with every update\"); RED4ext
|
|
217
|
-
"
|
|
219
|
+
"(\"prone to breaking with every update\"); RED4ext before 1.30.0 refuses a game patch it does " +
|
|
220
|
+
"not recognize (WopsS/RED4ext#33); 1.30.0 moved the check to each plugin's requested runtime " +
|
|
221
|
+
"(WopsS/RED4ext v1.30.0, src/dll/Systems/PluginSystem.cpp).",
|
|
218
222
|
},
|
|
219
223
|
];
|
|
220
224
|
export const cyberpunk2077Compat = {
|
|
@@ -137,8 +137,9 @@ const RED4EXT = [
|
|
|
137
137
|
explain: "RED4ext refuses to run against this game build, so no native plugin loads at " +
|
|
138
138
|
"all this launch. Everything downstream (TweakXL tweaks, ArchiveXL .xl files, " +
|
|
139
139
|
"Codeware-dependent scripts) is dead, and none of them will say so.",
|
|
140
|
-
hint: "Update RED4ext
|
|
141
|
-
"to the supported patch named
|
|
140
|
+
hint: "Update RED4ext (1.30.0 and later no longer refuse an unrecognized build; they " +
|
|
141
|
+
"check each plugin instead), or roll the game back to the supported patch named " +
|
|
142
|
+
"on the following line.",
|
|
142
143
|
seenIn: [
|
|
143
144
|
`${FIXTURES}/red4ext.synthetic.log`,
|
|
144
145
|
"https://github.com/WopsS/RED4ext/issues/33",
|
|
@@ -222,7 +223,7 @@ const REDSCRIPT = [
|
|
|
222
223
|
"or its data, not in compilation.",
|
|
223
224
|
seenIn: [
|
|
224
225
|
`${FIXTURES}/redscript_rCURRENT.ok.synthetic.log`,
|
|
225
|
-
"https://wiki.redmodding.org/redscript/help/troubleshooting",
|
|
226
|
+
"https://wiki.redmodding.org/scripting-cyberpunk/redscript/help/troubleshooting",
|
|
226
227
|
],
|
|
227
228
|
extract: ["output"],
|
|
228
229
|
},
|
|
@@ -59,7 +59,8 @@ export const cyberpunk2077 = {
|
|
|
59
59
|
extensions: [".archive", ".xl"],
|
|
60
60
|
ordered: true,
|
|
61
61
|
conflictScope: "opaque",
|
|
62
|
-
notes: "Loaded
|
|
62
|
+
notes: "Loaded in ASCII order of filename (modlist.txt overrides it when present); on identical " +
|
|
63
|
+
"resource paths the FIRST-loaded archive wins. " +
|
|
63
64
|
"A .xl sidecar is ArchiveXL and must keep the same base name as its .archive.",
|
|
64
65
|
},
|
|
65
66
|
{
|
|
@@ -169,6 +169,10 @@ const UNIQUE_IDS_EVIDENCE = [
|
|
|
169
169
|
"cp2077.sectors.sector-json-file-uniqueness",
|
|
170
170
|
"cp2077.toolchain.cli-convert-deserialize",
|
|
171
171
|
];
|
|
172
|
+
function idSpace(jsonPath) {
|
|
173
|
+
const at = jsonPath.lastIndexOf(".Data.Chunks[");
|
|
174
|
+
return at === -1 ? "$" : jsonPath.slice(0, at);
|
|
175
|
+
}
|
|
172
176
|
async function checkUniqueIds(text, root, file) {
|
|
173
177
|
const findings = [];
|
|
174
178
|
for (const key of ["HandleId", "BufferId"]) {
|
|
@@ -177,13 +181,15 @@ async function checkUniqueIds(text, root, file) {
|
|
|
177
181
|
for (const hit of hits) {
|
|
178
182
|
if (typeof hit.value !== "string")
|
|
179
183
|
continue;
|
|
180
|
-
const
|
|
184
|
+
const spaceKey = `${idSpace(hit.jsonPath)}\u0000${hit.value}`;
|
|
185
|
+
const list = byValue.get(spaceKey) ?? [];
|
|
181
186
|
list.push(hit);
|
|
182
|
-
byValue.set(
|
|
187
|
+
byValue.set(spaceKey, list);
|
|
183
188
|
}
|
|
184
|
-
for (const [
|
|
189
|
+
for (const [spaceKey, list] of byValue) {
|
|
185
190
|
if (list.length < 2)
|
|
186
191
|
continue;
|
|
192
|
+
const value = spaceKey.slice(spaceKey.indexOf("\u0000") + 1);
|
|
187
193
|
const lines = list.map((_, i) => lineOfOccurrence(text, `"${key}": "${value}"`, i)).filter((l) => l !== undefined);
|
|
188
194
|
findings.push({
|
|
189
195
|
validatorId: "cp2077.vehicles.cr2w-unique-ids",
|
|
@@ -117,7 +117,13 @@ export async function readModEngine2LoadOrder(install) {
|
|
|
117
117
|
const configPath = path.join(gameDir(install), MODENGINE2_CONFIG);
|
|
118
118
|
if (!(await pathExists(configPath)))
|
|
119
119
|
return { configPath, entries: [] };
|
|
120
|
-
|
|
120
|
+
let config;
|
|
121
|
+
try {
|
|
122
|
+
config = parseModEngine2Config(await fs.readFile(configPath, "utf8"));
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return { configPath, entries: [], error: `${configPath}: ${err.message}` };
|
|
126
|
+
}
|
|
121
127
|
const modDir = path.resolve(modengine2ModDir(install)).toLowerCase();
|
|
122
128
|
const entries = [];
|
|
123
129
|
let index = 0;
|
package/package.json
CHANGED
|
@@ -19,10 +19,13 @@ inputs:
|
|
|
19
19
|
name: version
|
|
20
20
|
prompt: Initial version (semver-ish, major.minor.revision).
|
|
21
21
|
default: "0.1.0"
|
|
22
|
+
# Interpolated raw into modwright.json; the same guard the Stardew scaffold carries.
|
|
23
|
+
pattern: "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:\\.(?:0|[1-9][0-9]*))?(?:-(?:[a-zA-Z0-9]+[-.]?)+)?$"
|
|
22
24
|
- type: string
|
|
23
25
|
name: author
|
|
24
|
-
prompt: Author name.
|
|
26
|
+
prompt: Author name (no double quotes or backslashes — it is written into modwright.json as-is).
|
|
25
27
|
default: "Unknown"
|
|
28
|
+
pattern: '^[^"\\\x00-\x1f]+$'
|
|
26
29
|
# A project can keep its
|
|
27
30
|
# LSLib divine path as a toolchain override in modwright.json; a fresh
|
|
28
31
|
# project on the same machine needs the same line or its first build
|
|
@@ -29,10 +29,13 @@ inputs:
|
|
|
29
29
|
name: version
|
|
30
30
|
prompt: Initial version (semver-ish, major.minor.revision).
|
|
31
31
|
default: "0.1.0"
|
|
32
|
+
# Interpolated raw into modwright.json; the same guard the Stardew scaffold carries.
|
|
33
|
+
pattern: "^(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)(?:\\.(?:0|[1-9][0-9]*))?(?:-(?:[a-zA-Z0-9]+[-.]?)+)?$"
|
|
32
34
|
- type: string
|
|
33
35
|
name: author
|
|
34
|
-
prompt: Author name.
|
|
36
|
+
prompt: Author name (no double quotes or backslashes — it is written into modwright.json as-is).
|
|
35
37
|
default: "Unknown"
|
|
38
|
+
pattern: '^[^"\\\x00-\x1f]+$'
|
|
36
39
|
# Observed in practice (2026-09-12): the WolvenKit CLI override had to be
|
|
37
40
|
# added to a fresh project by hand because the locator does not find a CLI
|
|
38
41
|
# installed outside the usual places. The path is machine-specific and
|