Release Healer Man 0.1.32

This commit is contained in:
phenom committed 2026-09-12 11:07:25 -04:00
1 parent d3d6aaebdb
commit 6dcb1034e5
102 files changed
+10772 -788

No files matched your search

+8 -7
View File
@@ -8,6 +8,7 @@ import { gunzipSync } from "node:zlib";
import { prepareContentObjects } from "./content-objects.mjs";
import { selectBootstrapContent } from "./prepare-bootstrap.mjs";
import { createContentManifest } from "./publish-content.mjs";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
test("ships one lossless object for identical assets and verifies original bytes", async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), "healer-content-compression-"));
@@ -36,17 +37,17 @@ test("ships one lossless object for identical assets and verifies original bytes
} finally { await rm(directory, { recursive: true, force: true }); }
});
test("bundles regular dungeons and exact map dependencies without installing Manastorm", () => {
const ids = ["core:ui", "dungeon:first", "dungeon:second", "shared:creatures", "manastorm:shared-map", "manastorm:optional-map", "characters:extra"];
test("bundles only starter packs and their exact dependencies", () => {
const ids = [...contentPolicy.bootstrapPackIds, "dungeon:second", "shared:creatures", "manastorm:shared-map", "manastorm:optional-map", "characters:extra"];
const files = ids.map((packId, index) => ({ logicalPath: `assets/${index}.glb`, packId, sha256: String(index).repeat(64), bytes: 100, extension: ".glb" }));
const result = selectBootstrapContent(files, [
{ id: "dungeon:first", dependencies: ["manastorm:shared-map", "shared:creatures"] },
{ id: "dungeon:second", dependencies: [] },
]);
assert.deepEqual(result.files.map((file) => file.packId), ids.slice(0, 5));
assert.deepEqual(result.files.map((file) => file.packId), contentPolicy.bootstrapPackIds);
const packs = new Map(result.packDefinitions.map((pack) => [pack.id, pack]));
assert.equal(packs.get("dungeon:first").bundledOnAndroid, true);
assert.equal(packs.get("dungeon:second").bundledOnAndroid, true);
assert.equal(packs.get("manastorm:shared-map").bundledOnAndroid, false);
assert.deepEqual([...packs.keys()].sort(), [...contentPolicy.bootstrapPackIds].sort());
assert.equal([...packs.values()].every((pack) => pack.bundledOnAndroid), true);
assert.equal(packs.has("dungeon:second"), false);
assert.equal(packs.has("manastorm:shared-map"), false);
assert.equal(packs.has("manastorm:optional-map"), false);
});
+55
View File
@@ -0,0 +1,55 @@
import { readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { brotliCompress, constants, gzip } from "node:zlib";
const compressBrotli = promisify(brotliCompress);
const compressGzip = promisify(gzip);
const ELIGIBLE_EXTENSIONS = new Set([".css", ".js", ".json"]);
const MINIMUM_BYTES = 1024;
async function eligibleFiles(root) {
const files = [];
async function visit(directory) {
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const filePath = path.join(directory, entry.name);
if (entry.isDirectory()) await visit(filePath);
else if (entry.isFile() && ELIGIBLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(filePath);
}
}
await visit(root);
return files;
}
export async function precompressStatic(root) {
const files = await eligibleFiles(root);
let sourceBytes = 0;
let brotliBytes = 0;
let gzipBytes = 0;
let writtenFiles = 0;
for (const filePath of files) {
const source = await readFile(filePath);
if (source.length < MINIMUM_BYTES) continue;
const [brotli, gzipped] = await Promise.all([
compressBrotli(source, { params: { [constants.BROTLI_PARAM_QUALITY]: 6 } }),
compressGzip(source, { level: 9 }),
]);
await Promise.all([
writeFile(`${filePath}.br`, brotli),
writeFile(`${filePath}.gz`, gzipped),
]);
sourceBytes += source.length;
brotliBytes += brotli.length;
gzipBytes += gzipped.length;
writtenFiles += 2;
}
return { sourceFiles: writtenFiles / 2, writtenFiles, sourceBytes, brotliBytes, gzipBytes };
}
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
const root = path.resolve(process.argv[2] ?? "dist");
const result = await precompressStatic(root);
console.log(JSON.stringify({ root, ...result }, null, 2));
}
+8 -5
View File
@@ -5,6 +5,7 @@ import {
createContentManifest, loadCampaignDungeonPackDefinitions, scanContentSources,
} from "./publish-content.mjs";
import { prepareContentObjects } from "./content-objects.mjs";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
export function selectBootstrapContent(files, packDefinitions) {
const manifest = createContentManifest(files, { packDefinitions });
@@ -15,15 +16,12 @@ export function selectBootstrapContent(files, packDefinitions) {
included.add(id);
for (const dependency of packs.get(id)?.dependencies ?? []) include(dependency);
}
for (const pack of manifest.packs) {
if (BUNDLED_ANDROID_PACKS.includes(pack.id) || pack.id.startsWith("dungeon:")) include(pack.id);
}
for (const packId of BUNDLED_ANDROID_PACKS) include(packId);
return {
files: files.filter((file) => included.has(file.packId)),
packDefinitions: manifest.packs.filter((pack) => included.has(pack.id)).map((pack) => ({
...pack,
// Shared maps can be physically bundled without installing Manastorm gameplay.
bundledOnAndroid: !pack.id.startsWith("manastorm:"),
bundledOnAndroid: BUNDLED_ANDROID_PACKS.includes(pack.id),
})),
};
}
@@ -44,6 +42,11 @@ export async function prepareBootstrap(platform) {
const prepared = await prepareContentObjects(selected.files, {
directory: path.join(outputDirectory, "content/bundled"), baseUrl: "/content/bundled",
});
if (prepared.storedBytes > contentPolicy.maxBootstrapStoredBytes) {
throw new Error(
`Bootstrap content is ${prepared.storedBytes} bytes; the release limit is ${contentPolicy.maxBootstrapStoredBytes} bytes.`,
);
}
const manifest = createContentManifest(prepared.files, {
baseUrl: "/content/bundled", packDefinitions: selected.packDefinitions,
});
+2 -8
View File
@@ -10,6 +10,7 @@ import {
import path from "node:path";
import { fileURLToPath } from "node:url";
import { prepareContentObjects } from "./content-objects.mjs";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
const SCRIPT_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
export const PROJECT_ROOT = path.resolve(SCRIPT_DIRECTORY, "../..");
@@ -23,14 +24,7 @@ const WOW_CAMPAIGN_CATALOG_PATH = path.join(
export const CONTENT_SCHEMA_VERSION = 1;
export const DEFAULT_MINIMUM_APP_VERSION = "0.1.0";
export const WOW_CAMPAIGN_DUNGEON_COUNT = 54;
export const BUNDLED_ANDROID_PACKS = Object.freeze([
"core:ui",
"core:equipment",
"core:character-basic",
"core:runtime",
"core:arenas",
"dungeon:wailing-caverns",
]);
export const BUNDLED_ANDROID_PACKS = Object.freeze([...contentPolicy.bootstrapPackIds]);
const RUNTIME_EXTENSIONS = new Set([
".avif", ".glb", ".js", ".json", ".ktx2", ".mp3", ".ogg", ".png",
+1 -1
View File
@@ -24,7 +24,7 @@ test("assigns stable player-facing packs from logical asset paths", () => {
assert.equal(contentPackForPath("assets/characters/wow335a-v1/orc/male/rig.glb"), "characters:wow335a-v1");
assert.equal(contentPackForPath("assets/game/manastorm/stage-1/visual.glb"), "manastorm:stage-1");
assert.equal(contentPackForPath("assets/game/arenas/nagrand-arena/visual.glb"), "core:arenas");
assert.equal(BUNDLED_ANDROID_PACKS.includes("core:arenas"), true);
assert.equal(BUNDLED_ANDROID_PACKS.includes("core:arenas"), false);
assert.equal(contentPackForPath("assets/game/combat-effects/audio/smite.wav"), "core:combat-media");
assert.equal(
contentPackForPath("assets/game/combat-effects/audio/manastorm/Manastorm_BossKill_GenericPortalsReady.wav"),
+14 -3
View File
@@ -7,6 +7,10 @@ import json
from pathlib import Path
from zipfile import ZIP_STORED, ZipFile
CONTENT_POLICY = json.loads(
(Path(__file__).resolve().parents[2] / "shared" / "content-policy.json").read_text(encoding="utf-8")
)
def verify_package(apk: Path) -> dict:
with ZipFile(apk) as archive:
@@ -35,13 +39,20 @@ def verify_package(apk: Path) -> dict:
if actual_objects != expected_objects:
raise ValueError("APK includes missing or unreferenced bundled objects.")
manastorms = sum(pack["id"].startswith("manastorm:") and pack["bundledOnAndroid"] for pack in manifest["packs"])
if dungeons != 84 or manastorms != 0:
raise ValueError(f"Unexpected bundled content: {dungeons} dungeons, {manastorms} Manastorm packs")
arenas = sum(pack["id"] == "core:arenas" for pack in manifest["packs"])
pack_ids = sorted(pack["id"] for pack in manifest["packs"])
expected_pack_ids = sorted(CONTENT_POLICY["bootstrapPackIds"])
if pack_ids != expected_pack_ids or dungeons != 1 or manastorms != 0 or arenas != 0:
raise ValueError(
f"Unexpected bundled content: {dungeons} dungeons, {manastorms} Manastorm packs, {arenas} arena packs"
)
if object_bytes > CONTENT_POLICY["maxBootstrapStoredBytes"]:
raise ValueError("APK bootstrap content exceeds the stored-byte budget.")
return {
"apk": str(apk.resolve()), "apkBytes": apk.stat().st_size,
"manifestVersion": manifest["version"], "logicalFiles": len(manifest["files"]),
"verifiedUniqueObjects": len(checked), "storedObjectBytes": object_bytes,
"bundledDungeons": dungeons, "bundledManastormPacks": manastorms,
"bundledDungeons": dungeons, "bundledManastormPacks": manastorms, "bundledArenaPacks": arenas,
"allOriginalHashesMatch": True,
}
+91
View File
@@ -0,0 +1,91 @@
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
const SCRIPT_PATH = fileURLToPath(import.meta.url);
const PROHIBITED_MODULE_PATTERNS = [
/(?:^|\/)src\/ArenaRuntime\.tsx$/,
/(?:^|\/)src\/app\/ShelvedModeRoutes\.tsx$/,
/(?:^|\/)src\/app\/ShelvedGameRuntime\.enabled\.tsx$/,
/(?:^|\/)src\/app\/ShelvedOnlineGroupRuntime\.enabled\.ts$/,
/(?:^|\/)src\/app\/(?:arenaClient|gmManastormRuntime|manastormShellRuntime)\.[jt]sx?$/,
/(?:^|\/)src\/game\/(?:arena|manastormAdmin|manastormCatalog|manastormInstalled)(?:\/|\.[jt]sx?$)/,
/(?:^|\/)src\/game\/ManastormBridge\.[jt]sx?$/,
/(?:^|\/)src\/ui\/(?:Arena|GmAdmin|GmReview|GmWorld|Manastorm)[^/]*\.[jt]sx?$/,
/(?:^|\/)src\/scene\/ShelvedGameScene\.enabled\.tsx$/,
/(?:^|\/)src\/game\/ShelvedDungeonSession\.enabled\.ts$/,
/(?:^|\/)src\/ui\/(?:ShelvedCompanionPanel|ShelvedHud)\.enabled\.tsx$/,
];
function fail(message) {
throw new Error(`Release bundle verification failed: ${message}`);
}
async function walkRelativeFiles(root) {
const files = [];
async function visit(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const filePath = path.join(directory, entry.name);
if (entry.isDirectory()) await visit(filePath);
else if (entry.isFile()) files.push(path.relative(root, filePath).split(path.sep).join("/"));
}
}
await visit(root);
return files;
}
export async function verifyReleaseBundle(root) {
const manifest = JSON.parse(await readFile(path.join(root, "content", "bootstrap-manifest.json"), "utf8"));
const moduleReport = JSON.parse(await readFile(path.join(root, "bundle-module-report.json"), "utf8"));
if (moduleReport.shelvedModesEnabled !== false) fail("normal releases must compile with shelved modes disabled");
const expectedPacks = [...contentPolicy.bootstrapPackIds].sort();
const actualPacks = (manifest.packs ?? []).map((pack) => pack.id).sort();
if (JSON.stringify(actualPacks) !== JSON.stringify(expectedPacks)) {
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))
));
if (hiddenPack) fail(`bootstrap contains hidden pack ${hiddenPack}`);
const objects = new Map();
for (const entry of Object.values(manifest.files ?? {})) {
const object = entry.download ?? entry;
if (!objects.has(object.url)) objects.set(object.url, object.bytes);
}
const storedBytes = [...objects.values()].reduce((sum, bytes) => sum + Number(bytes ?? 0), 0);
if (storedBytes > contentPolicy.maxBootstrapStoredBytes) {
fail(`bootstrap uses ${storedBytes} stored bytes; limit is ${contentPolicy.maxBootstrapStoredBytes}`);
}
const prohibitedModules = moduleReport.chunks.flatMap((chunk) => chunk.modules).filter((moduleId) => (
PROHIBITED_MODULE_PATTERNS.some((pattern) => pattern.test(moduleId))
));
if (prohibitedModules.length) fail(`shelved client modules were emitted: ${[...new Set(prohibitedModules)].join(", ")}`);
const files = await walkRelativeFiles(root);
const arenaAsset = files.find((file) => /(?:^|\/)assets\/game\/arenas(?:\/|$)/i.test(file));
if (arenaAsset) fail(`arena asset was emitted at ${arenaAsset}`);
for (const objectUrl of objects.keys()) {
const objectPath = path.join(root, objectUrl.replace(/^\/+/, ""));
const objectStats = await stat(objectPath);
if (!objectStats.isFile()) fail(`bootstrap object is missing: ${objectUrl}`);
}
return {
root,
packs: actualPacks,
storedBytes,
storedMiB: Number((storedBytes / 1024 / 1024).toFixed(2)),
objectCount: objects.size,
chunkCount: moduleReport.chunks.length,
};
}
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(SCRIPT_PATH)) {
const root = path.resolve(process.argv[2] ?? "dist");
console.log(JSON.stringify(await verifyReleaseBundle(root), null, 2));
}
@@ -0,0 +1,66 @@
import assert from "node:assert/strict";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import contentPolicy from "../../shared/content-policy.json" with { type: "json" };
import { verifyReleaseBundle } from "./verify-release-bundle.mjs";
async function fixture() {
const root = await mkdtemp(path.join(os.tmpdir(), "healer-man-release-bundle-test-"));
await mkdir(path.join(root, "content", "bundled"), { recursive: true });
const manifest = {
schemaVersion: 1,
packs: contentPolicy.bootstrapPackIds.map((id) => ({ id })),
files: {},
};
const moduleReport = { shelvedModesEnabled: false, chunks: [{ file: "assets/index.js", modules: ["src/App.tsx"] }] };
const write = async () => {
await writeFile(path.join(root, "content", "bootstrap-manifest.json"), `${JSON.stringify(manifest)}\n`);
await writeFile(path.join(root, "bundle-module-report.json"), `${JSON.stringify(moduleReport)}\n`);
};
await write();
return { root, manifest, moduleReport, write };
}
test("accepts the exact compact production policy", async () => {
const value = await fixture();
try {
const report = await verifyReleaseBundle(value.root);
assert.deepEqual(report.packs, [...contentPolicy.bootstrapPackIds].sort());
assert.equal(report.storedBytes, 0);
} finally {
await rm(value.root, { recursive: true, force: true });
}
});
test("rejects arena packs and shelved client modules", async () => {
const value = await fixture();
try {
value.manifest.packs.push({ id: "core:arenas" });
await value.write();
await assert.rejects(verifyReleaseBundle(value.root), /bootstrap packs/);
value.manifest.packs.pop();
value.moduleReport.chunks[0].modules.push("src/ArenaRuntime.tsx");
await value.write();
await assert.rejects(verifyReleaseBundle(value.root), /shelved client modules/);
} finally {
await rm(value.root, { recursive: true, force: true });
}
});
test("rejects a bootstrap above the stored-byte budget", async () => {
const value = await fixture();
try {
value.manifest.files["/large.glb"] = {
packId: "core:runtime",
url: `/content/bundled/${"c".repeat(64)}.glb`,
bytes: contentPolicy.maxBootstrapStoredBytes + 1,
};
await value.write();
await assert.rejects(verifyReleaseBundle(value.root), /limit is/);
} finally {
await rm(value.root, { recursive: true, force: true });
}
});