126 lines
4.7 KiB
JavaScript
126 lines
4.7 KiB
JavaScript
import { spawn as spawnChild } from "node:child_process";
|
|
import { stat } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
export const WOW_EXPORT_AUTOMATION_VERSION = "0.2.19";
|
|
export const WOW_EXPORT_REQUIRED_FLAGS = Object.freeze([
|
|
"--disable-auto-update",
|
|
"--discover-dungeon",
|
|
"--export-dungeon",
|
|
"--export-creatures",
|
|
]);
|
|
|
|
export function defaultWowExportExecutable(projectRoot) {
|
|
return path.resolve(
|
|
projectRoot,
|
|
`../wow335a/dungeon-export-work/tools/wow.export-${WOW_EXPORT_AUTOMATION_VERSION}/wow.export.exe`,
|
|
);
|
|
}
|
|
|
|
async function inspectCandidate(file, statFile) {
|
|
try {
|
|
const info = await statFile(file);
|
|
return info.isFile()
|
|
? { usable: true, reason: null }
|
|
: { usable: false, reason: "path exists but is not a file" };
|
|
} catch (error) {
|
|
if (error?.code === "ENOENT") return { usable: false, reason: "file does not exist" };
|
|
return { usable: false, reason: `cannot inspect file (${error?.code ?? error?.message ?? "unknown error"})` };
|
|
}
|
|
}
|
|
|
|
function unavailableMessage(checks, configured) {
|
|
const checked = checks.map(({ label, file, reason }) => ` - ${label}: ${file} (${reason})`).join("\n");
|
|
const selection = configured
|
|
? "WOW_EXPORT_BIN is set, so the default location was not used. Correct or unset that variable."
|
|
: "Restore the patched portable bundle at the default location, or set WOW_EXPORT_BIN to its exact executable path.";
|
|
return [
|
|
"HealerMan's patched wow.export automation bridge is unavailable.",
|
|
`Required build: wow.export ${WOW_EXPORT_AUTOMATION_VERSION} with these automation flags: ${WOW_EXPORT_REQUIRED_FLAGS.join(", ")}.`,
|
|
"Checked:",
|
|
checked,
|
|
selection,
|
|
"A stock GUI-only wow.export executable is not a drop-in replacement unless it includes the HealerMan automation patch.",
|
|
"No export was started and no placeholder assets were created.",
|
|
].join("\n");
|
|
}
|
|
|
|
export class WowExportUnavailableError extends Error {
|
|
constructor(checks, configured) {
|
|
super(unavailableMessage(checks, configured));
|
|
this.name = "WowExportUnavailableError";
|
|
this.code = "WOW_EXPORT_AUTOMATION_UNAVAILABLE";
|
|
this.checks = checks;
|
|
this.requiredVersion = WOW_EXPORT_AUTOMATION_VERSION;
|
|
this.requiredFlags = [...WOW_EXPORT_REQUIRED_FLAGS];
|
|
}
|
|
}
|
|
|
|
export async function resolveWowExportExecutable({
|
|
projectRoot,
|
|
environment = process.env,
|
|
statFile = stat,
|
|
} = {}) {
|
|
if (!projectRoot) throw new TypeError("resolveWowExportExecutable requires projectRoot.");
|
|
const configuredValue = environment.WOW_EXPORT_BIN?.trim();
|
|
const candidates = configuredValue
|
|
? [{ label: "WOW_EXPORT_BIN", file: path.resolve(projectRoot, configuredValue) }]
|
|
: [{ label: "default portable path", file: defaultWowExportExecutable(projectRoot) }];
|
|
const checks = [];
|
|
for (const candidate of candidates) {
|
|
const inspection = await inspectCandidate(candidate.file, statFile);
|
|
if (inspection.usable) return candidate.file;
|
|
checks.push({ ...candidate, reason: inspection.reason });
|
|
}
|
|
throw new WowExportUnavailableError(checks, Boolean(configuredValue));
|
|
}
|
|
|
|
function exporterFailureMessage(executable, argumentsList, detail) {
|
|
const operation = argumentsList.find((argument) => argument.startsWith("--")) ?? "unknown operation";
|
|
return [
|
|
`wow.export automation failed during ${operation}: ${detail}`,
|
|
`Executable: ${executable}`,
|
|
`Required automation flags: ${WOW_EXPORT_REQUIRED_FLAGS.join(", ")}.`,
|
|
"Verify that WOW_EXPORT_BIN points to the HealerMan-patched build, not the stock GUI-only release.",
|
|
"No placeholder assets were created.",
|
|
].join("\n");
|
|
}
|
|
|
|
export async function runWowExporter(argumentsList, {
|
|
projectRoot,
|
|
environment = process.env,
|
|
statFile = stat,
|
|
spawn = spawnChild,
|
|
} = {}) {
|
|
const executable = await resolveWowExportExecutable({ projectRoot, environment, statFile });
|
|
await new Promise((resolve, reject) => {
|
|
const child = spawn(executable, ["--disable-auto-update", ...argumentsList], {
|
|
cwd: path.dirname(executable),
|
|
stdio: "inherit",
|
|
windowsHide: true,
|
|
});
|
|
let settled = false;
|
|
const fail = (error) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
reject(error);
|
|
};
|
|
child.once("error", (error) => fail(new Error(exporterFailureMessage(
|
|
executable,
|
|
argumentsList,
|
|
`process could not start (${error?.code ?? error?.message ?? "unknown error"})`,
|
|
))));
|
|
child.once("exit", (code, signal) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
if (code === 0) resolve();
|
|
else reject(new Error(exporterFailureMessage(
|
|
executable,
|
|
argumentsList,
|
|
signal ? `process exited after signal ${signal}` : `process exited with code ${code}`,
|
|
)));
|
|
});
|
|
});
|
|
return executable;
|
|
}
|