modwright 0.1.2 → 0.1.4
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 +43 -4
- package/bridges/bg3-se/README.md +3 -2
- package/bridges/bg3-se/bridge.json +1 -1
- package/bridges/cp2077-cet/ModWrightBridge/probes.lua +228 -1
- package/bridges/cp2077-cet/README.md +6 -3
- package/bridges/cp2077-cet/bridge.json +2 -2
- package/dist/core/bridge/remove.js +48 -0
- package/dist/core/build/stage.js +3 -2
- package/dist/core/ini.js +26 -0
- package/dist/core/knowledge/facts.js +0 -12
- package/dist/core/logs/registry.js +19 -0
- package/dist/core/logs/scan.js +44 -5
- package/dist/core/logs/triage.js +79 -2
- package/dist/core/managers/mo2.js +121 -0
- package/dist/core/managers/vortex.js +52 -0
- package/dist/core/ownership/index.js +364 -0
- package/dist/core/project/index.js +1 -1
- package/dist/core/project/load.js +5 -0
- package/dist/core/testplan/registry.js +84 -2
- package/dist/core/writes/index.js +206 -0
- package/dist/index.js +0 -2
- package/dist/server.js +180 -15
- package/dist/surfaces/baldursgate3/surface.js +3 -0
- package/dist/surfaces/baldursgate3/validators/story.js +3 -3
- package/dist/surfaces/baldursgate3/writes.js +152 -0
- package/dist/surfaces/cyberpunk2077/index/parsers/tweakxl-yaml.js +2 -2
- package/dist/surfaces/cyberpunk2077/logs.js +82 -3
- package/dist/surfaces/cyberpunk2077/surface.js +11 -0
- package/dist/surfaces/cyberpunk2077/writes.js +67 -0
- package/dist/surfaces/skyrimse/loadorder.js +2 -26
- package/dist/surfaces/skyrimse/surface.js +1 -0
- package/dist/surfaces/stardewvalley/logs.js +41 -0
- package/dist/surfaces/stardewvalley/surface.js +2 -0
- package/dist/surfaces/stardewvalley/writes.js +136 -0
- package/knowledge/baldursgate3/dialogue.osiris-goals.yaml +29 -0
- package/knowledge/baldursgate3/osiris.tags.yaml +78 -0
- package/knowledge/cyberpunk2077/cet.natives.yaml +48 -0
- package/knowledge/cyberpunk2077/cet.rtti-binding.yaml +227 -0
- package/knowledge/cyberpunk2077/cet.sandbox.yaml +22 -0
- package/knowledge/cyberpunk2077/codeware.overview.yaml +63 -16
- package/knowledge/cyberpunk2077/codeware.systems.yaml +152 -33
- package/knowledge/cyberpunk2077/entities.queries.yaml +275 -0
- package/knowledge/cyberpunk2077/redhottools.hotreload.yaml +30 -4
- package/knowledge/cyberpunk2077/rtti.game-systems.yaml +224 -0
- package/package.json +1 -1
package/dist/core/logs/scan.js
CHANGED
|
@@ -37,14 +37,14 @@ function parseScanKey(key) {
|
|
|
37
37
|
}
|
|
38
38
|
export async function scanSource(source, signatures, scope, ctx) {
|
|
39
39
|
if (!source.exists) {
|
|
40
|
-
return { status: { source, linesScanned: 0, findingCount: 0 }, findings: [] };
|
|
40
|
+
return { status: { source, linesScanned: 0, findingCount: 0 }, findings: [], attributions: [] };
|
|
41
41
|
}
|
|
42
42
|
let stat;
|
|
43
43
|
try {
|
|
44
44
|
stat = await fs.stat(source.path);
|
|
45
45
|
}
|
|
46
46
|
catch {
|
|
47
|
-
return { status: { source, linesScanned: 0, findingCount: 0 }, findings: [] };
|
|
47
|
+
return { status: { source, linesScanned: 0, findingCount: 0 }, findings: [], attributions: [] };
|
|
48
48
|
}
|
|
49
49
|
const sizeBytes = stat.size;
|
|
50
50
|
const modifiedAt = stat.mtime;
|
|
@@ -63,7 +63,7 @@ export async function scanSource(source, signatures, scope, ctx) {
|
|
|
63
63
|
};
|
|
64
64
|
if (stale !== undefined)
|
|
65
65
|
status.stale = stale;
|
|
66
|
-
return { status, findings: [] };
|
|
66
|
+
return { status, findings: [], attributions: [] };
|
|
67
67
|
}
|
|
68
68
|
const birthtimeMs = Math.round(stat.birthtimeMs);
|
|
69
69
|
const mtimeMs = Math.round(stat.mtimeMs);
|
|
@@ -125,7 +125,7 @@ export async function scanSource(source, signatures, scope, ctx) {
|
|
|
125
125
|
};
|
|
126
126
|
if (stale !== undefined)
|
|
127
127
|
status.stale = stale;
|
|
128
|
-
return { status, findings: [] };
|
|
128
|
+
return { status, findings: [], attributions: [] };
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
if (startOffset === 0 && text.charCodeAt(0) === 0xfeff)
|
|
@@ -133,14 +133,18 @@ export async function scanSource(source, signatures, scope, ctx) {
|
|
|
133
133
|
let lines = splitLines(text);
|
|
134
134
|
if (clipped && lines.length > 0)
|
|
135
135
|
lines.shift();
|
|
136
|
+
let headTrimmed = clipped;
|
|
136
137
|
if ((!hadOffset || rotated) && lines.length > maxLines) {
|
|
137
138
|
lines = lines.slice(lines.length - maxLines);
|
|
139
|
+
headTrimmed = true;
|
|
138
140
|
}
|
|
139
141
|
const sinceDate = scope.since ? new Date(scope.since) : undefined;
|
|
140
142
|
let anyTimestampSeen = false;
|
|
141
143
|
let lastTimestamp;
|
|
142
144
|
const findingsBySignature = new Map();
|
|
143
145
|
const findings = [];
|
|
146
|
+
const attributions = [];
|
|
147
|
+
const context = {};
|
|
144
148
|
for (let i = 0; i < lines.length; i++) {
|
|
145
149
|
const line = lines[i];
|
|
146
150
|
if (sinceDate) {
|
|
@@ -157,6 +161,39 @@ export async function scanSource(source, signatures, scope, ctx) {
|
|
|
157
161
|
const match = sig.pattern.exec(line);
|
|
158
162
|
if (!match)
|
|
159
163
|
continue;
|
|
164
|
+
const spec = sig.attribution;
|
|
165
|
+
if (spec) {
|
|
166
|
+
const group = (name) => name === undefined ? undefined : match.groups?.[name];
|
|
167
|
+
if (spec.setsContext) {
|
|
168
|
+
const subject = group(spec.subject);
|
|
169
|
+
const actor = group(spec.actor);
|
|
170
|
+
if (subject !== undefined)
|
|
171
|
+
context.subject = subject;
|
|
172
|
+
if (actor !== undefined)
|
|
173
|
+
context.actor = actor;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const subject = group(spec.subject) ?? group(spec.subjectFallback) ?? spec.subjectLabel ?? context.subject;
|
|
177
|
+
if (subject !== undefined) {
|
|
178
|
+
const record = {
|
|
179
|
+
sourceId: source.id,
|
|
180
|
+
signatureId: sig.id,
|
|
181
|
+
kind: spec.kind,
|
|
182
|
+
subject,
|
|
183
|
+
actor: group(spec.actor) ?? context.actor ?? "?",
|
|
184
|
+
lineNumber: i + 1,
|
|
185
|
+
};
|
|
186
|
+
const via = group(spec.via);
|
|
187
|
+
if (via !== undefined)
|
|
188
|
+
record.via = via;
|
|
189
|
+
const detail = group(spec.detail);
|
|
190
|
+
if (detail !== undefined)
|
|
191
|
+
record.detail = detail;
|
|
192
|
+
attributions.push(record);
|
|
193
|
+
}
|
|
194
|
+
if (!spec.alsoFinding)
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
160
197
|
const existing = findingsBySignature.get(sig.id);
|
|
161
198
|
if (existing) {
|
|
162
199
|
existing.repeats = (existing.repeats ?? 0) + 1;
|
|
@@ -207,5 +244,7 @@ export async function scanSource(source, signatures, scope, ctx) {
|
|
|
207
244
|
status.rewritten = true;
|
|
208
245
|
if (sinceDate && !anyTimestampSeen)
|
|
209
246
|
status.timestamps = false;
|
|
210
|
-
|
|
247
|
+
if (headTrimmed)
|
|
248
|
+
status.headTrimmed = true;
|
|
249
|
+
return { status, findings, attributions };
|
|
211
250
|
}
|
package/dist/core/logs/triage.js
CHANGED
|
@@ -3,7 +3,9 @@ import { findRunningProcesses } from "../process.js";
|
|
|
3
3
|
import { resolveLogs } from "../surfaceutil.js";
|
|
4
4
|
import { scanSource } from "./scan.js";
|
|
5
5
|
const FINDINGS_CAP = 200;
|
|
6
|
-
|
|
6
|
+
const ATTRIBUTION_SUBJECT_CAP = 150;
|
|
7
|
+
const ATTRIBUTION_ACTOR_CAP = 100;
|
|
8
|
+
export async function triageLogs(surface, install, registry, scope = {}, options = {}) {
|
|
7
9
|
const sources = await resolveLogs(surface, install);
|
|
8
10
|
const processResult = await findRunningProcesses(surface.processNames ?? []);
|
|
9
11
|
const gameRunning = processResult.checked ? processResult.running.length > 0 : undefined;
|
|
@@ -34,15 +36,17 @@ export async function triageLogs(surface, install, registry, scope = {}) {
|
|
|
34
36
|
}
|
|
35
37
|
const statuses = [];
|
|
36
38
|
const allFindings = [];
|
|
39
|
+
const allAttributions = [];
|
|
37
40
|
const offsets = {};
|
|
38
41
|
const offsetKeys = {};
|
|
39
42
|
for (const source of sources) {
|
|
40
43
|
const signatures = registry.forSource(surface.id, source.id);
|
|
41
|
-
const { status, findings } = await scanSource(source, signatures, scope, { lastLaunch });
|
|
44
|
+
const { status, findings, attributions } = await scanSource(source, signatures, scope, { lastLaunch });
|
|
42
45
|
if (!source.exists && missingReasons[source.id])
|
|
43
46
|
status.missingReason = missingReasons[source.id];
|
|
44
47
|
statuses.push(status);
|
|
45
48
|
allFindings.push(...findings);
|
|
49
|
+
allAttributions.push(...attributions);
|
|
46
50
|
if (status.endOffset !== undefined)
|
|
47
51
|
offsets[source.id] = status.endOffset;
|
|
48
52
|
if (status.key !== undefined)
|
|
@@ -104,8 +108,81 @@ export async function triageLogs(surface, install, registry, scope = {}) {
|
|
|
104
108
|
verdict.truncated = true;
|
|
105
109
|
if (Object.keys(systemNotes).length > 0)
|
|
106
110
|
verdict.systemNotes = systemNotes;
|
|
111
|
+
if (allAttributions.length > 0) {
|
|
112
|
+
verdict.attribution = buildAttributionReport(allAttributions, statuses, options.attributionFor);
|
|
113
|
+
}
|
|
107
114
|
return verdict;
|
|
108
115
|
}
|
|
116
|
+
export function buildAttributionReport(records, statuses, filter) {
|
|
117
|
+
const needle = filter?.trim().toLowerCase();
|
|
118
|
+
const kept = needle
|
|
119
|
+
? records.filter((r) => [r.subject, r.actor, r.via].some((v) => v !== undefined && v.toLowerCase().includes(needle)))
|
|
120
|
+
: records;
|
|
121
|
+
const bySubject = new Map();
|
|
122
|
+
for (const record of kept) {
|
|
123
|
+
let entry = bySubject.get(record.subject);
|
|
124
|
+
if (!entry) {
|
|
125
|
+
entry = { subject: record.subject, kinds: [], actors: [], order: bySubject.size };
|
|
126
|
+
bySubject.set(record.subject, entry);
|
|
127
|
+
}
|
|
128
|
+
if (!entry.kinds.includes(record.kind))
|
|
129
|
+
entry.kinds.push(record.kind);
|
|
130
|
+
const same = entry.actors.find((a) => a.actor === record.actor && a.via === record.via && a.kind === record.kind && a.detail === record.detail);
|
|
131
|
+
if (same) {
|
|
132
|
+
same.count++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const actor = {
|
|
136
|
+
actor: record.actor,
|
|
137
|
+
kind: record.kind,
|
|
138
|
+
sourceId: record.sourceId,
|
|
139
|
+
firstLine: record.lineNumber,
|
|
140
|
+
count: 1,
|
|
141
|
+
};
|
|
142
|
+
if (record.via !== undefined)
|
|
143
|
+
actor.via = record.via;
|
|
144
|
+
if (record.detail !== undefined)
|
|
145
|
+
actor.detail = record.detail;
|
|
146
|
+
entry.actors.push(actor);
|
|
147
|
+
}
|
|
148
|
+
const distinctActors = (subject) => new Set(subject.actors.map((a) => `${a.actor}\u0000${a.via ?? ""}`)).size;
|
|
149
|
+
const isContested = (subject) => !subject.kinds.every((k) => k === "read-order") && distinctActors(subject) > 1;
|
|
150
|
+
const all = [...bySubject.values()];
|
|
151
|
+
const contested = all.filter(isContested).length;
|
|
152
|
+
all.sort((a, b) => Number(isContested(b)) - Number(isContested(a)) || a.order - b.order);
|
|
153
|
+
const subjects = all.slice(0, ATTRIBUTION_SUBJECT_CAP).map(({ order: _order, ...subject }) => {
|
|
154
|
+
if (subject.actors.length <= ATTRIBUTION_ACTOR_CAP)
|
|
155
|
+
return subject;
|
|
156
|
+
return {
|
|
157
|
+
...subject,
|
|
158
|
+
actors: subject.actors.slice(0, ATTRIBUTION_ACTOR_CAP),
|
|
159
|
+
omittedActors: subject.actors.length - ATTRIBUTION_ACTOR_CAP,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
const report = {
|
|
163
|
+
records: records.length,
|
|
164
|
+
contested,
|
|
165
|
+
subjects,
|
|
166
|
+
note: "Straight from the logs: each actor is who the log line itself names, listed in the order " +
|
|
167
|
+
"the log shows them acting, so for edits and reads the later actor applied last. Only what " +
|
|
168
|
+
"the frameworks chose to log is here; a mod that changes something without the loader " +
|
|
169
|
+
"logging it will not appear.",
|
|
170
|
+
};
|
|
171
|
+
if (needle)
|
|
172
|
+
report.filter = filter.trim();
|
|
173
|
+
if (all.length > ATTRIBUTION_SUBJECT_CAP)
|
|
174
|
+
report.omittedSubjects = all.length - ATTRIBUTION_SUBJECT_CAP;
|
|
175
|
+
const partial = statuses
|
|
176
|
+
.filter((st) => st.headTrimmed && records.some((r) => r.sourceId === st.source.id))
|
|
177
|
+
.map((st) => st.source.id);
|
|
178
|
+
if (partial.length > 0) {
|
|
179
|
+
report.partialSources = partial;
|
|
180
|
+
report.note +=
|
|
181
|
+
` The start of ${partial.join(", ")} was not read (tail cap), so earlier attribution lines ` +
|
|
182
|
+
"are missing; pass a larger maxLines (up to 20000) for the whole run.";
|
|
183
|
+
}
|
|
184
|
+
return report;
|
|
185
|
+
}
|
|
109
186
|
function buildHeadline(sources, statuses, findings, systems) {
|
|
110
187
|
if (sources.length === 0) {
|
|
111
188
|
return "No log sources are declared for this game; triage has nothing to read.";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { localAppData, pathExists } from "../fsutil.js";
|
|
4
|
+
import { parseIni } from "../ini.js";
|
|
5
|
+
function iniPath(value) {
|
|
6
|
+
return value.replace(/\\\\/g, "\\");
|
|
7
|
+
}
|
|
8
|
+
function samePath(a, b) {
|
|
9
|
+
const norm = (p) => path.resolve(iniPath(p)).replace(/[\\/]+$/, "").toLowerCase();
|
|
10
|
+
return norm(a) === norm(b);
|
|
11
|
+
}
|
|
12
|
+
export async function readMo2Instance(iniFile, portable) {
|
|
13
|
+
let ini;
|
|
14
|
+
try {
|
|
15
|
+
ini = parseIni(await fs.readFile(iniFile, "utf8"));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const instanceDir = path.dirname(iniFile);
|
|
21
|
+
const general = ini.get("general");
|
|
22
|
+
const settings = ini.get("settings");
|
|
23
|
+
const base = iniPath(settings?.get("base_directory") ?? instanceDir);
|
|
24
|
+
const configured = (key, fallback) => path.resolve(instanceDir, iniPath(settings?.get(key) ?? `%BASE_DIR%/${fallback}`).replace(/%BASE_DIR%/gi, base));
|
|
25
|
+
const profileName = general?.get("selected_profile") ?? "Default";
|
|
26
|
+
const profilesDir = configured("profiles_directory", "profiles");
|
|
27
|
+
const gamePath = general?.get("gamepath");
|
|
28
|
+
const instance = {
|
|
29
|
+
instanceDir,
|
|
30
|
+
iniPath: iniFile,
|
|
31
|
+
portable,
|
|
32
|
+
profileName,
|
|
33
|
+
profileDir: path.join(profilesDir, profileName),
|
|
34
|
+
modsDir: configured("mod_directory", "mods"),
|
|
35
|
+
overwriteDir: configured("overwrite_directory", "overwrite"),
|
|
36
|
+
downloadsDir: configured("download_directory", "downloads"),
|
|
37
|
+
};
|
|
38
|
+
if (gamePath)
|
|
39
|
+
instance.gamePath = iniPath(gamePath);
|
|
40
|
+
return instance;
|
|
41
|
+
}
|
|
42
|
+
export async function findMo2Instance(install, options = {}) {
|
|
43
|
+
if (options.instancePath) {
|
|
44
|
+
const iniFile = options.instancePath.toLowerCase().endsWith(".ini")
|
|
45
|
+
? options.instancePath
|
|
46
|
+
: path.join(options.instancePath, "ModOrganizer.ini");
|
|
47
|
+
if (await pathExists(iniFile))
|
|
48
|
+
return readMo2Instance(iniFile, true);
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const root = options.instancesRoot ?? localAppData("ModOrganizer");
|
|
52
|
+
let names;
|
|
53
|
+
try {
|
|
54
|
+
names = await fs.readdir(root);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
const iniFile = path.join(root, name, "ModOrganizer.ini");
|
|
61
|
+
if (!(await pathExists(iniFile)))
|
|
62
|
+
continue;
|
|
63
|
+
const instance = await readMo2Instance(iniFile, false);
|
|
64
|
+
if (instance?.gamePath && samePath(instance.gamePath, install.root))
|
|
65
|
+
return instance;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
export function parseMo2Modlist(text) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
73
|
+
const line = raw.replace(/^/, "");
|
|
74
|
+
if (!line.trim() || line.startsWith("#"))
|
|
75
|
+
continue;
|
|
76
|
+
const marker = line[0];
|
|
77
|
+
const hasMarker = marker === "+" || marker === "-" || marker === "*";
|
|
78
|
+
const name = (hasMarker ? line.slice(1) : line).trim();
|
|
79
|
+
if (!name || seen.has(name))
|
|
80
|
+
continue;
|
|
81
|
+
seen.add(name);
|
|
82
|
+
lines.push({ name, enabled: marker !== "-", foreign: marker === "*" });
|
|
83
|
+
}
|
|
84
|
+
return lines.map((entry, index) => ({
|
|
85
|
+
...entry,
|
|
86
|
+
separator: entry.name.endsWith("_separator"),
|
|
87
|
+
priority: lines.length - index,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
export async function readMo2Modlist(profileDir) {
|
|
91
|
+
try {
|
|
92
|
+
return parseMo2Modlist(await fs.readFile(path.join(profileDir, "modlist.txt"), "utf8"));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export async function readMo2ModMeta(modDir) {
|
|
99
|
+
let ini;
|
|
100
|
+
try {
|
|
101
|
+
ini = parseIni(await fs.readFile(path.join(modDir, "meta.ini"), "utf8"));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
const general = ini.get("general");
|
|
107
|
+
const meta = {};
|
|
108
|
+
const modId = Number(general?.get("modid"));
|
|
109
|
+
if (Number.isInteger(modId) && modId > 0)
|
|
110
|
+
meta.nexusModId = modId;
|
|
111
|
+
const version = general?.get("version");
|
|
112
|
+
if (version)
|
|
113
|
+
meta.version = version;
|
|
114
|
+
const installationFile = general?.get("installationfile");
|
|
115
|
+
if (installationFile)
|
|
116
|
+
meta.installationFile = installationFile;
|
|
117
|
+
const gameName = general?.get("gamename");
|
|
118
|
+
if (gameName)
|
|
119
|
+
meta.gameName = gameName;
|
|
120
|
+
return meta;
|
|
121
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
const MANIFEST = /^vortex\.deployment\.(?:(?<type>[^.]+)\.)?json$/i;
|
|
4
|
+
export async function readVortexManifests(dir) {
|
|
5
|
+
const manifests = [];
|
|
6
|
+
const unreadable = [];
|
|
7
|
+
let names;
|
|
8
|
+
try {
|
|
9
|
+
names = await fs.readdir(dir);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return { manifests, unreadable };
|
|
13
|
+
}
|
|
14
|
+
for (const name of names.sort()) {
|
|
15
|
+
const match = MANIFEST.exec(name);
|
|
16
|
+
if (!match)
|
|
17
|
+
continue;
|
|
18
|
+
const file = path.join(dir, name);
|
|
19
|
+
try {
|
|
20
|
+
const raw = JSON.parse(await fs.readFile(file, "utf8"));
|
|
21
|
+
if (!Array.isArray(raw.files))
|
|
22
|
+
throw new Error("no files array");
|
|
23
|
+
const files = [];
|
|
24
|
+
for (const entry of raw.files) {
|
|
25
|
+
if (typeof entry?.relPath !== "string" || typeof entry.source !== "string")
|
|
26
|
+
continue;
|
|
27
|
+
const deployed = { relPath: entry.relPath, source: entry.source };
|
|
28
|
+
if (Array.isArray(entry.merged))
|
|
29
|
+
deployed.merged = entry.merged.filter((m) => typeof m === "string");
|
|
30
|
+
if (typeof entry.target === "string" && entry.target)
|
|
31
|
+
deployed.target = entry.target;
|
|
32
|
+
if (typeof entry.time === "number")
|
|
33
|
+
deployed.time = entry.time;
|
|
34
|
+
files.push(deployed);
|
|
35
|
+
}
|
|
36
|
+
const manifest = { file, deployDir: dir, modType: match.groups?.type ?? "", files };
|
|
37
|
+
if (typeof raw.gameId === "string")
|
|
38
|
+
manifest.gameId = raw.gameId;
|
|
39
|
+
if (typeof raw.deploymentMethod === "string")
|
|
40
|
+
manifest.deploymentMethod = raw.deploymentMethod;
|
|
41
|
+
if (typeof raw.deploymentTime === "number")
|
|
42
|
+
manifest.deploymentTime = raw.deploymentTime;
|
|
43
|
+
if (typeof raw.stagingPath === "string")
|
|
44
|
+
manifest.stagingPath = raw.stagingPath;
|
|
45
|
+
manifests.push(manifest);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
unreadable.push({ file, error: error.message });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { manifests, unreadable };
|
|
52
|
+
}
|