Update Healer Man server
This commit is contained in:
1 parent
a64b2cb575
commit
71246e5b75
502 files changed
+4245
-19
No files matched your search
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { verifiedRuneWakerParticlePhases } from "./rom-particle-readiness.mjs";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { MpqArchive } = require("stormlib-js");
|
||||
@@ -520,6 +521,19 @@ export function parseRunewakerImageRecords(buffer, requestedImageIds) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function runewakerSoundPathsForModel(modelPath, resourceRoot) {
|
||||
if (!/^model\/fx\/.+\.(?:ros|ras)$/i.test(modelPath) || modelPath.includes("..")) return [];
|
||||
const soundPath = modelPath.replace(/^model\//i, "sound/");
|
||||
const stem = soundPath.replace(/\.(?:ros|ras)$/i, "");
|
||||
for (const candidate of [stem, soundPath]) {
|
||||
for (const extension of ["mp3", "wav", "ogg"]) {
|
||||
const sourcePath = `${candidate}.${extension}`;
|
||||
if (fs.existsSync(path.join(resourceRoot, ...sourcePath.split("/")))) return [sourcePath];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function runewakerImageIdsFromMagicCollect(buffer, spellIds) {
|
||||
const count = buffer.readInt32LE(132);
|
||||
const recordSize = 1032;
|
||||
@@ -534,7 +548,39 @@ function runewakerImageIdsFromMagicCollect(buffer, spellIds) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function attachVerifiedRuneWakerParticles(entries) {
|
||||
const archivePath = path.join(projectRoot, "public/assets/game/combat-effects/original/runewaker/manifest.json");
|
||||
if (!fs.existsSync(archivePath)) return;
|
||||
const archive = JSON.parse(fs.readFileSync(archivePath, "utf8"));
|
||||
const decoded = new Map((archive.results ?? [])
|
||||
.filter((result) => result.rendererReady && result.graphUrl)
|
||||
.map((result) => [result.model.toLowerCase(), result]));
|
||||
const bySpellId = new Map(entries.map((entry) => [entry.spellId, entry]));
|
||||
for (const { spellId, phase, model, scale, loopCastSound } of verifiedRuneWakerParticlePhases) {
|
||||
const entry = bySpellId.get(spellId);
|
||||
const asset = entry?.phases?.[phase]?.find((candidate) => (
|
||||
candidate.model.toLowerCase() === model
|
||||
));
|
||||
const result = decoded.get(model);
|
||||
if (!asset || !result) continue;
|
||||
const graphPath = path.join(projectRoot, "public", result.graphUrl.slice(1));
|
||||
if (!fs.existsSync(graphPath)) continue;
|
||||
const graph = JSON.parse(fs.readFileSync(graphPath, "utf8"));
|
||||
if (graph.sourceModel !== model || !graph.rendererReady) continue;
|
||||
asset.effectKind = "runewaker-particles";
|
||||
asset.effectGraphUrl = result.graphUrl;
|
||||
if (scale) asset.scale = scale;
|
||||
if (loopCastSound) for (const sound of entry.sounds ?? []) {
|
||||
if (sound.phase === "cast-start"
|
||||
&& sound.sourcePath === "sound/fx/skill/holy/act_casting_c_hand.wav") {
|
||||
sound.loopDuringCast = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildRunewakerEntries(spellIds, imageBySpellId, dataRoot) {
|
||||
const resourceRoot = path.dirname(dataRoot);
|
||||
const magicCollect = fs.readFileSync(path.join(dataRoot, "magiccollectobject.db"));
|
||||
const collectedImages = runewakerImageIdsFromMagicCollect(magicCollect, spellIds);
|
||||
for (const [spellId, imageId] of collectedImages) if (!imageBySpellId.has(spellId)) imageBySpellId.set(spellId, imageId);
|
||||
@@ -554,25 +600,35 @@ function buildRunewakerEntries(spellIds, imageBySpellId, dataRoot) {
|
||||
}
|
||||
if (!/\.(?:ros|ras)$/i.test(sourcePath)) continue;
|
||||
const lower = sourcePath.toLowerCase();
|
||||
const phase = /(?:^|_)t(?:_|\.)|explode|impact|hit/.test(lower)
|
||||
const filename = path.posix.basename(lower);
|
||||
const phase = /(?:^|_)t(?:_|\.)|explode|impact/.test(filename)
|
||||
|| (/hit/.test(filename) && !/(?:^|_)f(?:_|\.)/.test(filename))
|
||||
? "impact"
|
||||
: /(?:^|_)f(?:_|\.)|fly|missile/.test(lower)
|
||||
: /(?:^|_)f(?:_|\.)|fly|missile/.test(filename)
|
||||
? "release"
|
||||
: /buff|aura|extend/.test(lower)
|
||||
: /buff|aura|extend/.test(filename)
|
||||
? "aura-start"
|
||||
: "cast-start";
|
||||
(phases[phase] ??= []).push({ model: sourcePath });
|
||||
for (const soundPath of runewakerSoundPathsForModel(sourcePath, resourceRoot)) {
|
||||
sounds.push({ sourcePath: soundPath, phase, weight: 1, volume: 1 });
|
||||
}
|
||||
}
|
||||
if (!Object.keys(phases).length && !sounds.length) continue;
|
||||
for (const [phase, assets] of Object.entries(phases)) {
|
||||
phases[phase] = [...new Map(assets.map((asset) => [asset.model.toLowerCase(), asset])).values()];
|
||||
}
|
||||
const uniqueSounds = [...new Map(sounds.map((sound) => [`${sound.phase ?? "any"}:${sound.sourcePath.toLowerCase()}`, sound])).values()];
|
||||
if (!Object.keys(phases).length && !uniqueSounds.length) continue;
|
||||
entries.push({
|
||||
key: `runewaker:${spellId}`,
|
||||
source: "runewaker",
|
||||
spellId,
|
||||
imageId,
|
||||
phases,
|
||||
...(sounds.length ? { sounds } : {}),
|
||||
...(uniqueSounds.length ? { sounds: uniqueSounds } : {}),
|
||||
});
|
||||
}
|
||||
attachVerifiedRuneWakerParticles(entries);
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -622,7 +678,9 @@ function packageAudioAssets(entries, options) {
|
||||
?? path.join(projectRoot, "..", "wow335a", "Data"));
|
||||
const runewakerResourceRoot = path.resolve(options.runewakerResourceRoot
|
||||
?? path.join(projectRoot, "..", "Runewaker", "Resource"));
|
||||
const archives = openedWowAudioArchives(wowDataRoot);
|
||||
const archives = entries.some((entry) => entry.source !== "runewaker")
|
||||
? openedWowAudioArchives(wowDataRoot)
|
||||
: [];
|
||||
const sourceCache = new Map();
|
||||
const missing = [];
|
||||
const packagedFiles = new Set();
|
||||
@@ -736,12 +794,55 @@ export function generateCombatEffectManifest(options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateRunewakerCombatEffectManifest(options = {}) {
|
||||
const output = path.resolve(options.output ?? path.join(projectRoot, "public", "assets", "game", "combat-effects", "manifest.json"));
|
||||
const previous = JSON.parse(fs.readFileSync(output, "utf8"));
|
||||
const runewakerData = path.resolve(options.runewakerData ?? path.join(projectRoot, "..", "Runewaker", "Resource", "data"));
|
||||
const { requested, runewakerImageBySpellId } = collectAbilitySpellIds();
|
||||
const runewakerEntries = buildRunewakerEntries(
|
||||
requested.filter((entry) => entry.source === "runewaker").map((entry) => entry.spellId),
|
||||
runewakerImageBySpellId,
|
||||
runewakerData,
|
||||
);
|
||||
const audio = options.packageAudio
|
||||
? packageAudioAssets(runewakerEntries, { ...options, runewakerResourceRoot: path.dirname(runewakerData) })
|
||||
: { missing: [] };
|
||||
const entries = [...previous.entries.filter((entry) => entry.source !== "runewaker"), ...runewakerEntries]
|
||||
.sort((left, right) => left.source.localeCompare(right.source) || left.spellId - right.spellId);
|
||||
const mapped = new Set(entries.map((entry) => entry.key));
|
||||
const oldRunewakerRequested = previous.entries.filter((entry) => entry.source === "runewaker").length
|
||||
+ previous.coverage.fallbackKeys.filter((key) => key.startsWith("runewaker:")).length;
|
||||
const runewakerRequested = requested.filter((entry) => entry.source === "runewaker");
|
||||
const fallbackKeys = [
|
||||
...previous.coverage.fallbackKeys.filter((key) => !key.startsWith("runewaker:")),
|
||||
...runewakerRequested.map((entry) => `runewaker:${entry.spellId}`).filter((key) => !mapped.has(key)),
|
||||
];
|
||||
const soundPaths = new Set(entries.flatMap((entry) => (entry.sounds ?? []).map((sound) => `${entry.source}:${sound.sourcePath}`)));
|
||||
const audioUrls = new Set(entries.flatMap((entry) => (entry.sounds ?? []).map((sound) => sound.url).filter(Boolean)));
|
||||
return {
|
||||
...previous,
|
||||
sources: previous.sources.map((source) => source.kind === "runewaker"
|
||||
? { kind: "runewaker", path: path.relative(projectRoot, runewakerData).replaceAll("\\", "/"), sha256: sha256(fs.readFileSync(path.join(runewakerData, "imageobject.db"))) }
|
||||
: source),
|
||||
coverage: {
|
||||
...previous.coverage,
|
||||
requested: previous.coverage.requested - oldRunewakerRequested + runewakerRequested.length,
|
||||
authentic: entries.length,
|
||||
fallback: fallbackKeys.length,
|
||||
fallbackKeys,
|
||||
audio: { referenced: soundPaths.size, packaged: audioUrls.size, missing: audio.missing },
|
||||
},
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
function parseArguments(arguments_) {
|
||||
const options = {};
|
||||
for (let index = 0; index < arguments_.length; index += 1) {
|
||||
const argument = arguments_[index];
|
||||
if (argument === "--audit") options.audit = true;
|
||||
else if (argument === "--package-audio") options.packageAudio = true;
|
||||
else if (argument === "--runewaker-only") options.runewakerOnly = true;
|
||||
else if (argument === "--output") options.output = arguments_[++index];
|
||||
else if (argument === "--wow-archive") options.wowArchive = arguments_[++index];
|
||||
else if (argument === "--ascension-root") options.ascensionRoot = arguments_[++index];
|
||||
@@ -754,7 +855,9 @@ function parseArguments(arguments_) {
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
const output = path.resolve(options.output ?? path.join(projectRoot, "public", "assets", "game", "combat-effects", "manifest.json"));
|
||||
const manifest = generateCombatEffectManifest(options);
|
||||
const manifest = options.runewakerOnly
|
||||
? updateRunewakerCombatEffectManifest(options)
|
||||
: generateCombatEffectManifest(options);
|
||||
if (options.audit && manifest.coverage.audio.missing.length) {
|
||||
throw new Error(`Combat effects audit failed: ${manifest.coverage.audio.missing.length} referenced audio files could not be packaged.`);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user