Update Healer Man server

This commit is contained in:
phenom committed 2026-09-24 16:42:43 -04:00
1 parent a64b2cb575
commit 71246e5b75
502 files changed
+4245 -19

No files matched your search

+110 -7
View File
@@ -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.`);
}