Update Healer Man server
This commit is contained in:
1 parent
71246e5b75
commit
a868401ebc
40 files changed
+1522
-161
No files matched your search
@@ -534,6 +534,38 @@ export function runewakerSoundPathsForModel(modelPath, resourceRoot) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// These one-shot cues are direct 3D audio events (ID 43) in the named ROS
|
||||
// motions. The client places the impact models through the skill's MagicObject
|
||||
// component image, while the projectile model comes from ImageObject.DB.
|
||||
// The projectile's embedded cue differs from the parallel sound/fx filename.
|
||||
const verifiedRuneWakerEventSounds = [
|
||||
{
|
||||
spellId: 491147, phase: "impact",
|
||||
model: "model/fx/skill/holy/act_benedictory_heal_t_down.ros",
|
||||
sourcePath: "sound/fx/skill/holy/act_benedictory_heal_t_down.mp3",
|
||||
},
|
||||
{
|
||||
spellId: 490256, phase: "release",
|
||||
model: "model/fx/skill/ice/act_icecone01_f.ros",
|
||||
sourcePath: "sound/fx/skill/ice/icenew/act_icecone01_f.ros.mp3",
|
||||
},
|
||||
{
|
||||
spellId: 490256, phase: "impact",
|
||||
model: "model/fx/skill/ice/act_riptide_t_freeze_hit.ros",
|
||||
sourcePath: "sound/fx/skill/ice/act_riptide_t_freeze_hit.mp3",
|
||||
},
|
||||
];
|
||||
|
||||
function attachVerifiedRuneWakerEventSounds(entries) {
|
||||
const bySpellId = new Map(entries.map((entry) => [entry.spellId, entry]));
|
||||
for (const { spellId, phase, model, sourcePath } of verifiedRuneWakerEventSounds) {
|
||||
const entry = bySpellId.get(spellId);
|
||||
if (!entry?.phases?.[phase]?.some((asset) => asset.model?.toLowerCase() === model)) continue;
|
||||
if (entry.sounds?.some((sound) => sound.phase === phase && sound.sourcePath?.toLowerCase() === sourcePath)) continue;
|
||||
(entry.sounds ??= []).push({ sourcePath, phase, weight: 1, volume: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
function runewakerImageIdsFromMagicCollect(buffer, spellIds) {
|
||||
const count = buffer.readInt32LE(132);
|
||||
const recordSize = 1032;
|
||||
@@ -548,27 +580,82 @@ function runewakerImageIdsFromMagicCollect(buffer, spellIds) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// The cast's MagicCollect ImageID omits the target-side ExtendACT. These two
|
||||
// component links are verified against MagicObject.ImageID and ImageObject.DB
|
||||
// before the target model is added to the combat presentation manifest.
|
||||
const verifiedRuneWakerTargetEffects = [
|
||||
{ spellId: 491147, componentId: 501543, imageId: 573048,
|
||||
model: "model/fx/skill/holy/act_benedictory_heal_t_down.ros", point: "p_down" },
|
||||
{ spellId: 490256, componentId: 620408, imageId: 572062,
|
||||
model: "model/fx/skill/ice/act_riptide_t_freeze_hit.ros", point: "freeze_hit" },
|
||||
];
|
||||
|
||||
function verifiedRunewakerTargetModels(magicCollect, magicObject, imageObject, spellIds) {
|
||||
const wanted = verifiedRuneWakerTargetEffects.filter((effect) => spellIds.includes(effect.spellId));
|
||||
if (!wanted.length) return new Map();
|
||||
const collectOffsets = new Map();
|
||||
for (let offset = 140; offset + 1032 <= magicCollect.length; offset += 1032) {
|
||||
const spellId = magicCollect.readInt32LE(offset);
|
||||
if (wanted.some((effect) => effect.spellId === spellId)) collectOffsets.set(spellId, offset);
|
||||
}
|
||||
const componentImages = new Map();
|
||||
for (let offset = 140; offset + 992 <= magicObject.length; offset += 992) {
|
||||
const componentId = magicObject.readInt32LE(offset);
|
||||
if (wanted.some((effect) => effect.componentId === componentId)) {
|
||||
componentImages.set(componentId, magicObject.readInt32LE(offset + 0x20));
|
||||
}
|
||||
}
|
||||
const componentImageFields = new Map();
|
||||
for (let offset = 140; offset + 3712 <= imageObject.length; offset += 3712) {
|
||||
const imageId = imageObject.readInt32LE(offset);
|
||||
if (!wanted.some((effect) => effect.imageId === imageId)) continue;
|
||||
const textField = (fieldOffset, length) => imageObject.subarray(
|
||||
offset + fieldOffset, offset + fieldOffset + length,
|
||||
).toString("latin1").split("\0", 1)[0].replaceAll("\\", "/").toLowerCase();
|
||||
componentImageFields.set(imageId, {
|
||||
extendAct: textField(2132, 128),
|
||||
extendPoint: textField(2260, 32),
|
||||
});
|
||||
}
|
||||
for (const effect of wanted) {
|
||||
const collectOffset = collectOffsets.get(effect.spellId);
|
||||
const componentIds = collectOffset === undefined ? [] : Array.from(
|
||||
{ length: (0x2c0 - 0x180) / 4 + 1 }, (_, index) =>
|
||||
magicCollect.readInt32LE(collectOffset + 0x180 + index * 4),
|
||||
);
|
||||
if (!componentIds.includes(effect.componentId)
|
||||
|| componentImages.get(effect.componentId) !== effect.imageId
|
||||
|| componentImageFields.get(effect.imageId)?.extendAct !== effect.model
|
||||
|| componentImageFields.get(effect.imageId)?.extendPoint !== effect.point) {
|
||||
throw new Error(`RuneWaker target media source link changed for spell ${effect.spellId}`);
|
||||
}
|
||||
}
|
||||
return new Map(wanted.map((effect) => [effect.spellId, effect]));
|
||||
}
|
||||
|
||||
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)
|
||||
.filter((result) => (result.rendererReady || result.partialRendererReady) && 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) {
|
||||
for (const { spellId, phase, model, scale, loopCastSound, partialVisual } 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;
|
||||
if (!asset || !result || (partialVisual ? !result.partialRendererReady : !result.rendererReady)) 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;
|
||||
if (graph.sourceModel !== model
|
||||
|| (partialVisual ? !graph.partialRendererReady : !graph.rendererReady)) continue;
|
||||
asset.effectKind = "runewaker-particles";
|
||||
asset.effectGraphUrl = result.graphUrl;
|
||||
if (partialVisual) asset.visualCompleteness = "partial";
|
||||
if (scale) asset.scale = scale;
|
||||
if (loopCastSound) for (const sound of entry.sounds ?? []) {
|
||||
if (sound.phase === "cast-start"
|
||||
@@ -582,15 +669,29 @@ function attachVerifiedRuneWakerParticles(entries) {
|
||||
function buildRunewakerEntries(spellIds, imageBySpellId, dataRoot) {
|
||||
const resourceRoot = path.dirname(dataRoot);
|
||||
const magicCollect = fs.readFileSync(path.join(dataRoot, "magiccollectobject.db"));
|
||||
const magicObject = fs.readFileSync(path.join(dataRoot, "magicobject.db"));
|
||||
const imageObject = fs.readFileSync(path.join(dataRoot, "imageobject.db"));
|
||||
const targetEffects = verifiedRunewakerTargetModels(magicCollect, magicObject, imageObject, spellIds);
|
||||
const collectedImages = runewakerImageIdsFromMagicCollect(magicCollect, spellIds);
|
||||
for (const [spellId, imageId] of collectedImages) if (!imageBySpellId.has(spellId)) imageBySpellId.set(spellId, imageId);
|
||||
const requestedImages = [...new Set(spellIds.map((id) => imageBySpellId.get(id)).filter(Boolean))];
|
||||
const images = parseRunewakerImageRecords(fs.readFileSync(path.join(dataRoot, "imageobject.db")), requestedImages);
|
||||
const requestedImages = [...new Set([
|
||||
...spellIds.map((id) => imageBySpellId.get(id)).filter(Boolean),
|
||||
...[...targetEffects.values()].map((effect) => effect.imageId),
|
||||
])];
|
||||
const images = parseRunewakerImageRecords(imageObject, requestedImages);
|
||||
const entries = [];
|
||||
for (const spellId of spellIds) {
|
||||
const imageId = imageBySpellId.get(spellId);
|
||||
if (!imageId) continue;
|
||||
const paths = images.get(imageId) ?? [];
|
||||
const targetEffect = targetEffects.get(spellId);
|
||||
const targetImagePaths = targetEffect ? images.get(targetEffect.imageId) ?? [] : [];
|
||||
const targetModel = targetEffect && targetImagePaths.find((candidate) => (
|
||||
candidate.toLowerCase() === targetEffect.model
|
||||
));
|
||||
if (targetEffect && !targetModel) {
|
||||
throw new Error(`RuneWaker target ImageObject changed for spell ${spellId}`);
|
||||
}
|
||||
const paths = [...(images.get(imageId) ?? []), ...(targetModel ? [targetModel] : [])];
|
||||
const phases = {};
|
||||
const sounds = [];
|
||||
for (const sourcePath of paths) {
|
||||
@@ -628,6 +729,7 @@ function buildRunewakerEntries(spellIds, imageBySpellId, dataRoot) {
|
||||
...(uniqueSounds.length ? { sounds: uniqueSounds } : {}),
|
||||
});
|
||||
}
|
||||
attachVerifiedRuneWakerEventSounds(entries);
|
||||
attachVerifiedRuneWakerParticles(entries);
|
||||
return entries;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user