Update Healer Man server

This commit is contained in:
phenom committed 2026-09-20 18:08:43 -04:00
1 parent 3e30af4c98
commit 97ee0c13f9
25 files changed
+576 -179

No files matched your search

+21
View File
@@ -2,6 +2,7 @@ import { Capacitor } from "@capacitor/core";
import {
appVersionMeetsMinimum,
changedFilesForPack,
contentPackIdsForLogicalUrls,
filesForContentPack,
parseContentManifest,
type ContentManifest,
@@ -636,6 +637,26 @@ export async function installContentPack(packId: string): Promise<void> {
}
}
/** Installs only the packs owning the selected run's logical model URLs. */
export async function installContentForLogicalUrls(logicalUrls: readonly string[]): Promise<void> {
await validateInstalledContentStorage();
const logicalPaths = logicalUrls.map((logicalUrl) => {
try { return new URL(logicalUrl, "https://healer-man.invalid").pathname; } catch { return logicalUrl; }
});
const alreadyAvailable = logicalPaths.every((logicalPath) => (
Boolean(snapshot.bundledManifest?.files[logicalPath])
|| Boolean(installedState.files[logicalPath] && validatedInstalledFiles.has(logicalPath))
));
if (alreadyAvailable) return;
const manifest = snapshot.manifest ?? await checkForContentUpdates();
if (!manifest) throw new Error(snapshot.error || "Content server is unavailable.");
for (const packId of contentPackIdsForLogicalUrls(manifest, logicalUrls)) {
if (!contentPackIsUsable(packId) || contentPackHasUpdate(packId)) {
await installContentPack(packId);
}
}
}
export async function removeContentPack(packId: string): Promise<void> {
if (contentPackIsBundled(packId)) return;
const remainingFiles = { ...installedState.files };
+25
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
appVersionMeetsMinimum,
changedFilesForPack,
contentPackIdsForLogicalUrls,
filesForContentPack,
parseContentManifest,
type ContentManifest,
@@ -46,4 +47,28 @@ describe("content manifest", () => {
"/assets/a.glb": { sha256: "a".repeat(64) },
}).map(([logicalPath]) => logicalPath)).toEqual(["/assets/b.glb"]);
});
it("resolves a seeded run's logical models to distinct owning packs", () => {
const withSecondPack: ContentManifest = {
...manifest,
packs: [...manifest.packs, {
id: "shared:creatures", label: "Shared", bytes: 1, fileCount: 1,
bundledOnAndroid: false, dependencies: [],
}],
files: {
...manifest.files,
"/assets/shared.glb": {
packId: "shared:creatures", url: "/content/objects/shared.glb",
sha256: "c".repeat(64), bytes: 1,
},
},
};
expect(contentPackIdsForLogicalUrls(withSecondPack, [
"/assets/shared.glb",
"/assets/a.glb",
"/assets/shared.glb?cache=1",
])).toEqual(["dungeon:test", "shared:creatures"]);
expect(() => contentPackIdsForLogicalUrls(withSecondPack, ["/assets/missing.glb"]))
.toThrow(/missing randomized asset/i);
});
});
+20
View File
@@ -32,6 +32,26 @@ export interface ContentManifest {
readonly files: Readonly<Record<string, ContentManifestFile>>;
}
/** Resolves runtime logical asset URLs to the independently installable packs that own them. */
export function contentPackIdsForLogicalUrls(
manifest: ContentManifest,
logicalUrls: readonly string[],
): readonly string[] {
const packIds = new Set<string>();
for (const logicalUrl of logicalUrls) {
let pathname: string;
try {
pathname = new URL(logicalUrl, "https://healer-man.invalid").pathname;
} catch {
throw new Error(`Invalid content asset URL: ${logicalUrl}`);
}
const entry = manifest.files[pathname];
if (!entry) throw new Error(`The content catalog is missing randomized asset ${pathname}.`);
packIds.add(entry.packId);
}
return [...packIds].sort();
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}