Update Healer Man server
This commit is contained in:
1 parent
97ee0c13f9
commit
f864b5aea7
20 files changed
+468
-211
No files matched your search
@@ -17,7 +17,7 @@ test("ships one lossless object for identical assets and verifies original bytes
|
||||
const original = Buffer.from("original geometry, textures, and animation\n".repeat(4096));
|
||||
await writeFile(sourcePath, original);
|
||||
const common = { sourcePath, bytes: original.length, sha256: createHash("sha256").update(original).digest("hex"), extension: ".glb" };
|
||||
const files = ["first", "second"].map((id) => ({ ...common, logicalPath: `assets/creatures/${id}/model.glb`, packId: `dungeon:${id}` }));
|
||||
const files = ["first", "second"].map((id) => ({ ...common, logicalPath: `assets/creatures/${id}/model.glb`, packId: `creatures:${id}` }));
|
||||
const result = await prepareContentObjects(files, { directory: path.join(directory, "objects"), baseUrl: "/content/bundled" });
|
||||
assert.equal(result.uniqueObjects, 1);
|
||||
assert.equal(result.uniqueBytes, original.length);
|
||||
|
||||
@@ -78,6 +78,7 @@ function packLabel(packId) {
|
||||
const [category, id = category] = packId.split(":", 2);
|
||||
const name = id.split("-").map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`).join(" ");
|
||||
if (category === "dungeon") return name;
|
||||
if (category === "creatures") return `${name} Creatures`;
|
||||
if (packId === "characters:wow335a-v1") return "Extra Wrath Character Models & Skins";
|
||||
if (packId === "characters:ascension-v1") return "Ascension Character Models & Skins";
|
||||
if (packId === "characters:runewaker-v1") return "RuneWaker Character Models & Skins";
|
||||
@@ -107,12 +108,13 @@ export function contentPackForPath(logicalPath) {
|
||||
return basicWrathHuman ? "core:character-basic" : `characters:${segments[2]}`;
|
||||
}
|
||||
if (segments[1] === "creatures" && segments[2]) {
|
||||
return segments[2] === "shared" ? "shared:creatures" : `dungeon:${segments[2]}`;
|
||||
return segments[2] === "shared" ? "shared:creatures" : `creatures:${segments[2]}`;
|
||||
}
|
||||
if (segments[1] === "game" && segments[2] === "creatures" && segments[3]) {
|
||||
return segments[3] === "shared" ? "shared:creatures" : `dungeon:${segments[3]}`;
|
||||
return segments[3] === "shared" ? "shared:creatures" : `creatures:${segments[3]}`;
|
||||
}
|
||||
if (segments[1] === "game" && segments[2] === "dungeons" && segments[3]) {
|
||||
if (segments[4] === "creatures") return `creatures:${segments[3]}`;
|
||||
return `dungeon:${segments[3]}`;
|
||||
}
|
||||
if (segments[1] === "game" && segments[2] === "arenas") return "core:arenas";
|
||||
@@ -203,44 +205,58 @@ function assetPackId(url) {
|
||||
|
||||
/**
|
||||
* Builds the logical install topology for the 54-dungeon WoW campaign.
|
||||
* Environment files intentionally remain in their independently downloadable
|
||||
* Manastorm packs; a dungeon pack owns its creatures and pulls in exactly the
|
||||
* environment/shared-creature packs referenced by the generated definition.
|
||||
* Environment and creature files remain independently downloadable. A dungeon
|
||||
* pack pulls in exactly the environment and creature packs referenced by its
|
||||
* generated definition, while randomized modes can install creatures alone.
|
||||
*/
|
||||
export function campaignDungeonPackDefinitions(definitions) {
|
||||
const packs = new Map();
|
||||
for (const definition of definitions) {
|
||||
const dungeonPackId = `dungeon:${definition.id}`;
|
||||
if (definition.id === "wailing-caverns") {
|
||||
const referencedCreaturePacks = new Set([
|
||||
"creatures:wailing-caverns",
|
||||
...Object.values(definition.entities ?? {})
|
||||
.map((entity) => entity.visual?.model?.url)
|
||||
.filter(Boolean)
|
||||
.map(assetPackId),
|
||||
]);
|
||||
const unexpectedCreaturePacks = [...referencedCreaturePacks]
|
||||
.filter((id) => id !== "creatures:wailing-caverns" && id !== "shared:creatures");
|
||||
if (unexpectedCreaturePacks.length) {
|
||||
throw new Error(`${definition.id} references unexpected creature packs: ${unexpectedCreaturePacks.join(", ")}`);
|
||||
}
|
||||
packs.set(dungeonPackId, {
|
||||
id: dungeonPackId,
|
||||
label: definition.title ?? packLabel(dungeonPackId),
|
||||
bundledOnAndroid: true,
|
||||
dependencies: [],
|
||||
dependencies: [...referencedCreaturePacks],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const environmentUrls = [
|
||||
...(definition.assets?.visual ?? []).map((asset) => asset.url),
|
||||
...(definition.assets?.collision ?? []).map((asset) => asset.url),
|
||||
definition.assets?.navigation?.url,
|
||||
].filter(Boolean);
|
||||
const resolvedEnvironmentPacks = environmentUrls.map(assetPackId);
|
||||
if (!resolvedEnvironmentPacks.length || resolvedEnvironmentPacks.some((id) => !id?.startsWith("manastorm:"))) {
|
||||
throw new Error(`${definition.id} has an environment asset outside a Manastorm content pack.`);
|
||||
if (!resolvedEnvironmentPacks.length
|
||||
|| resolvedEnvironmentPacks.some((id) => !id?.startsWith("manastorm:"))) {
|
||||
throw new Error(`${definition.id} has an environment asset outside its expected content pack.`);
|
||||
}
|
||||
const environmentPackIds = new Set(resolvedEnvironmentPacks);
|
||||
if (environmentPackIds.size !== 1) {
|
||||
throw new Error(`${definition.id} must resolve to exactly one Manastorm environment pack.`);
|
||||
}
|
||||
const [environmentPackId] = environmentPackIds;
|
||||
packs.set(environmentPackId, {
|
||||
id: environmentPackId,
|
||||
label: packLabel(environmentPackId),
|
||||
bundledOnAndroid: false,
|
||||
dependencies: [],
|
||||
});
|
||||
if (environmentPackId !== dungeonPackId) {
|
||||
packs.set(environmentPackId, {
|
||||
id: environmentPackId,
|
||||
label: packLabel(environmentPackId),
|
||||
bundledOnAndroid: false,
|
||||
dependencies: [],
|
||||
});
|
||||
}
|
||||
|
||||
const creatureUrls = Object.values(definition.entities ?? {})
|
||||
.map((entity) => entity.visual?.model?.url)
|
||||
@@ -249,16 +265,21 @@ export function campaignDungeonPackDefinitions(definitions) {
|
||||
if (resolvedCreaturePacks.some((id) => !id)) {
|
||||
throw new Error(`${definition.id} has a creature asset outside a content pack.`);
|
||||
}
|
||||
const referencedCreaturePacks = new Set(resolvedCreaturePacks.filter((id) => id !== dungeonPackId));
|
||||
const unexpectedCreaturePacks = [...referencedCreaturePacks].filter((id) => id !== "shared:creatures");
|
||||
const referencedCreaturePacks = new Set(resolvedCreaturePacks);
|
||||
const expectedCreaturePackId = `creatures:${definition.id}`;
|
||||
const unexpectedCreaturePacks = [...referencedCreaturePacks]
|
||||
.filter((id) => id !== expectedCreaturePackId && id !== "shared:creatures");
|
||||
if (unexpectedCreaturePacks.length) {
|
||||
throw new Error(`${definition.id} references unexpected creature packs: ${unexpectedCreaturePacks.join(", ")}`);
|
||||
}
|
||||
packs.set(dungeonPackId, {
|
||||
id: dungeonPackId,
|
||||
label: definition.title ?? packLabel(dungeonPackId),
|
||||
bundledOnAndroid: false,
|
||||
dependencies: [environmentPackId, ...referencedCreaturePacks],
|
||||
bundledOnAndroid: definition.id === "wailing-caverns",
|
||||
dependencies: [
|
||||
...(environmentPackId === dungeonPackId ? [] : [environmentPackId]),
|
||||
...referencedCreaturePacks,
|
||||
],
|
||||
});
|
||||
}
|
||||
return [...packs.values()].sort((left, right) => left.id.localeCompare(right.id));
|
||||
@@ -350,6 +371,15 @@ export function createContentManifest(files, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// RuneWaker dungeon definitions are generated TypeScript rather than part of
|
||||
// the WoW JSON catalog. Their matching creature dependency is recoverable
|
||||
// from the stable pack namespace without importing runtime code here.
|
||||
for (const creaturePack of [...packs.values()].filter((pack) => pack.id.startsWith("creatures:"))) {
|
||||
const dungeonPack = packs.get(`dungeon:${creaturePack.id.slice("creatures:".length)}`);
|
||||
if (!dungeonPack || dungeonPack.dependencies.includes(creaturePack.id)) continue;
|
||||
dungeonPack.dependencies = [...dungeonPack.dependencies, creaturePack.id];
|
||||
}
|
||||
|
||||
const sortedPacks = [...packs.values()].sort((left, right) => left.id.localeCompare(right.id));
|
||||
for (const pack of sortedPacks) {
|
||||
const objects = new Map(files.filter((file) => file.packId === pack.id)
|
||||
|
||||
@@ -26,7 +26,9 @@ test("gives randomized bite-sized its base location dependency closure", () => {
|
||||
|
||||
test("assigns stable player-facing packs from logical asset paths", () => {
|
||||
assert.equal(contentPackForPath("assets/game/dungeons/wailing-caverns/visual.glb"), "dungeon:wailing-caverns");
|
||||
assert.equal(contentPackForPath("assets/creatures/wailing-caverns/mutanus.glb"), "dungeon:wailing-caverns");
|
||||
assert.equal(contentPackForPath("assets/creatures/wailing-caverns/mutanus.glb"), "creatures:wailing-caverns");
|
||||
assert.equal(contentPackForPath("assets/game/creatures/deadmines/mine-rat.glb"), "creatures:deadmines");
|
||||
assert.equal(contentPackForPath("assets/game/dungeons/deadmines/creatures/mr-smite.glb"), "creatures:deadmines");
|
||||
assert.equal(contentPackForPath("assets/characters/wow335a-v1/manifest.json"), "core:character-basic");
|
||||
assert.equal(contentPackForPath("assets/characters/wow335a-v1/human/male/rig.glb"), "core:character-basic");
|
||||
assert.equal(contentPackForPath("assets/characters/wow335a-v1/textures/character/human/male/humanmaleskin00_00.webp"), "core:character-basic");
|
||||
@@ -44,6 +46,27 @@ test("assigns stable player-facing packs from logical asset paths", () => {
|
||||
assert.equal(contentPackForPath("basis/basis_transcoder.wasm"), "core:runtime");
|
||||
});
|
||||
|
||||
test("keeps RuneWaker environments and creatures in independently installable packs", () => {
|
||||
const files = [
|
||||
"assets/game/dungeons/forsaken-abbey/visual.glb",
|
||||
"assets/creatures/forsaken-abbey/ancalon.glb",
|
||||
].map((logicalPath, index) => ({
|
||||
logicalPath,
|
||||
sourcePath: logicalPath,
|
||||
sha256: String(index + 1).repeat(64),
|
||||
bytes: 100,
|
||||
extension: ".glb",
|
||||
packId: contentPackForPath(logicalPath),
|
||||
}));
|
||||
const manifest = createContentManifest(files, { version: "runewaker-creature-split" });
|
||||
const packs = new Map(manifest.packs.map((pack) => [pack.id, pack]));
|
||||
|
||||
assert.deepEqual(packs.get("dungeon:forsaken-abbey")?.dependencies, ["creatures:forsaken-abbey"]);
|
||||
assert.equal(packs.get("dungeon:forsaken-abbey")?.fileCount, 1);
|
||||
assert.equal(packs.get("creatures:forsaken-abbey")?.fileCount, 1);
|
||||
assert.equal(manifest.files["/assets/creatures/forsaken-abbey/ancalon.glb"].packId, "creatures:forsaken-abbey");
|
||||
});
|
||||
|
||||
test("gives every WoW campaign dungeon an exact install dependency closure", async () => {
|
||||
const campaign = JSON.parse(await readFile(path.join(
|
||||
PROJECT_ROOT,
|
||||
@@ -113,6 +136,7 @@ test("gives every WoW campaign dungeon an exact install dependency closure", asy
|
||||
packId: contentPackForPath(logicalPath),
|
||||
});
|
||||
addFile("assets/game/dungeons/wailing-caverns/visual.glb");
|
||||
for (const creature of wailingCreatureManifest.creatures) addFile(creature.url.slice(1));
|
||||
for (const dungeon of campaign.definitions) {
|
||||
for (const asset of [
|
||||
...dungeon.assets.visual,
|
||||
@@ -134,20 +158,27 @@ test("gives every WoW campaign dungeon an exact install dependency closure", asy
|
||||
const pack = packs.get(`dungeon:${dungeon.id}`);
|
||||
assert.ok(pack, dungeon.id);
|
||||
if (dungeon.id === "wailing-caverns") {
|
||||
const creaturePacks = [...new Set([
|
||||
"creatures:wailing-caverns",
|
||||
...Object.values(dungeon.entities).flatMap((entity) => (
|
||||
entity.visual?.model?.url ? [contentPackForPath(entity.visual.model.url.slice(1))] : []
|
||||
)),
|
||||
])];
|
||||
assert.equal(pack.bundledOnAndroid, true);
|
||||
assert.deepEqual(pack.dependencies, []);
|
||||
assert.deepEqual(pack.dependencies, creaturePacks);
|
||||
continue;
|
||||
}
|
||||
const environmentPack = contentPackForPath(dungeon.assets.visual[0].url.slice(1));
|
||||
const needsSharedCreatures = Object.values(dungeon.entities).some((entity) => (
|
||||
entity.visual?.model?.url?.startsWith("/assets/game/creatures/shared/")
|
||||
));
|
||||
const creaturePacks = [...new Set(Object.values(dungeon.entities).flatMap((entity) => (
|
||||
entity.visual?.model?.url ? [contentPackForPath(entity.visual.model.url.slice(1))] : []
|
||||
)))];
|
||||
assert.deepEqual(
|
||||
pack.dependencies,
|
||||
[environmentPack, ...(needsSharedCreatures ? ["shared:creatures"] : [])],
|
||||
[...(environmentPack === pack.id ? [] : [environmentPack]), ...creaturePacks],
|
||||
dungeon.id,
|
||||
);
|
||||
assert.deepEqual(packs.get(environmentPack)?.dependencies, [], dungeon.id);
|
||||
assert.equal(pack.bundledOnAndroid, dungeon.id === "wailing-caverns");
|
||||
if (environmentPack !== pack.id) assert.deepEqual(packs.get(environmentPack)?.dependencies, [], dungeon.id);
|
||||
}
|
||||
|
||||
for (const [dungeonId, environmentPack] of [
|
||||
|
||||
@@ -46,8 +46,9 @@ export async function verifyReleaseBundle(root) {
|
||||
fail(`bootstrap packs were ${actualPacks.join(", ")}; expected ${expectedPacks.join(", ")}`);
|
||||
}
|
||||
const hiddenPack = actualPacks.find((packId) => (
|
||||
contentPolicy.hiddenContentPackIds.includes(packId)
|
||||
|| contentPolicy.hiddenContentPackPrefixes.some((prefix) => packId.startsWith(prefix))
|
||||
!contentPolicy.bootstrapPackIds.includes(packId)
|
||||
&& (contentPolicy.hiddenContentPackIds.includes(packId)
|
||||
|| contentPolicy.hiddenContentPackPrefixes.some((prefix) => packId.startsWith(prefix)))
|
||||
));
|
||||
if (hiddenPack) fail(`bootstrap contains hidden pack ${hiddenPack}`);
|
||||
|
||||
|
||||
Reference in new issue
Block a user