| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
- import { tmpdir } from "node:os";
- import { join, resolve } from "node:path";
- import { describe, expect, it } from "vitest";
- import {
- ContentLoader,
- ContentRegistry,
- HookRegistry,
- ModLoader,
- TimeSystem,
- World,
- contentSchemas,
- createEngine,
- type ContentBundle,
- type ContentKind,
- type HookContext,
- } from "../src/index.js";
- const repoRoot = resolve(process.cwd(), "../..");
- const contentDir = resolve(repoRoot, "examples", "sample-game", "content");
- function modSource(id: string, tag: string): string {
- return [
- "export default {",
- ` manifest: { id: "${id}", version: "1.0.0" },`,
- " hooks: {",
- " onDayStart(ctx) {",
- " const world = ctx.world;",
- ` const stats = world.get("${tag}") ?? world.create({ id: "${tag}", type: "mod-state", fired: 0 });`,
- " stats.fired += 1;",
- " },",
- " },",
- "};",
- "",
- ].join("\n");
- }
- async function modsFixture(mods: Record<string, string>): Promise<string> {
- const dir = await mkdtemp(join(tmpdir(), "core-enabled-mods-"));
- for (const [id, source] of Object.entries(mods)) {
- const target = join(dir, id);
- await mkdir(target, { recursive: true });
- await writeFile(join(target, "index.mjs"), source, "utf8");
- }
- return dir;
- }
- async function rmRecursive(dir: string): Promise<void> {
- await rm(dir, { recursive: true, force: true });
- }
- function makeCtx(): HookContext {
- return {
- world: new World(),
- registry: new ContentRegistry(),
- time: new TimeSystem(),
- emit: () => {},
- };
- }
- describe("ModLoader enabledMods filter (F1)", () => {
- it("loads only the listed mods and binds only their hooks", async () => {
- const dir = await modsFixture({ "a-mod": modSource("a-mod", "mod:a-stats"), "b-mod": modSource("b-mod", "mod:b-stats") });
- try {
- const hooks = new HookRegistry();
- const loader = new ModLoader(hooks);
- const statuses = await loader.load(dir, ["a-mod"]);
- expect(statuses.map((s) => s.id)).toEqual(["a-mod"]);
- expect(statuses[0]?.loaded).toBe(true);
- hooks.fire("onDayStart", makeCtx());
- expect(hooks.bound("onDayStart")).toBe(true);
- } finally {
- await rmRecursive(dir);
- }
- });
- it("undefined enabledMods loads everything (backward compatible)", async () => {
- const dir = await modsFixture({ "a-mod": modSource("a-mod", "mod:a-stats"), "b-mod": modSource("b-mod", "mod:b-stats") });
- try {
- const hooks = new HookRegistry();
- const loader = new ModLoader(hooks);
- const statuses = await loader.load(dir);
- expect(statuses.map((s) => s.id).sort()).toEqual(["a-mod", "b-mod"]);
- } finally {
- await rmRecursive(dir);
- }
- });
- it("unknown ids in the list are ignored, not errors", async () => {
- const dir = await modsFixture({ "a-mod": modSource("a-mod", "mod:a-stats") });
- try {
- const hooks = new HookRegistry();
- const loader = new ModLoader(hooks);
- const statuses = await loader.load(dir, ["ghost", "a-mod"]);
- expect(statuses.map((s) => s.id)).toEqual(["a-mod"]);
- const none = await loader.load(dir, ["ghost"]);
- expect(none).toEqual([]);
- } finally {
- await rmRecursive(dir);
- }
- });
- });
- describe("createEngine enabledMods (F1)", () => {
- it("excludes disabled mods from engine.mods and their hooks never fire", async () => {
- const dir = await modsFixture({ "a-mod": modSource("a-mod", "mod:a-stats"), "b-mod": modSource("b-mod", "mod:b-stats") });
- try {
- const engine = createEngine({ contentDir, modsDir: dir, enabledMods: ["a-mod"] });
- await engine.start();
- try {
- expect(engine.enabledMods).toEqual(["a-mod"]);
- expect(engine.mods.map((m) => m.id)).toEqual(["a-mod"]);
- engine.tick(480);
- expect(engine.world.get("mod:a-stats")?.fired).toBe(1);
- expect(engine.world.get("mod:b-stats")).toBeUndefined();
- } finally {
- await engine.stop();
- }
- } finally {
- await rmRecursive(dir);
- }
- });
- it("reload respects the same filter and a disabled id reload reports not loaded", async () => {
- const dir = await modsFixture({ "a-mod": modSource("a-mod", "mod:a-stats"), "b-mod": modSource("b-mod", "mod:b-stats") });
- try {
- const engine = createEngine({ contentDir, modsDir: dir, enabledMods: ["a-mod"] });
- await engine.start();
- try {
- const statuses = await engine.reloadMods();
- expect(statuses.map((s) => s.id)).toEqual(["a-mod"]);
- const single = await engine.reloadMods("b-mod");
- expect(single[0]?.loaded).toBe(false);
- expect(single[0]?.errors?.[0]).toContain("mod not loaded");
- } finally {
- await engine.stop();
- }
- } finally {
- await rmRecursive(dir);
- }
- });
- it("bundle mode filters bundled mods too", async () => {
- const { readFile } = await import("node:fs/promises");
- const source = await readFile(resolve(repoRoot, "examples", "sample-game", "mods", "sample-game-mod", "index.mjs"), "utf8");
- const registry = new ContentRegistry();
- const loader = new ContentLoader(registry);
- const report = await loader.load(contentDir);
- if (!report.ok) throw new Error("sample content load failed");
- const content: Record<string, unknown[]> = {};
- for (const kind of Object.keys(contentSchemas) as ContentKind[]) {
- const defs = registry.list(kind);
- if (defs.length > 0) content[kind] = defs;
- }
- const bundle: ContentBundle = { format: "nengine-bundle", version: 1, content };
- bundle.mods = {
- "sample-game-mod": { manifest: { id: "sample-game-mod", version: "0.1.0" }, code: source },
- "other-mod": { manifest: { id: "other-mod", version: "0.1.0" }, code: `export default { manifest: { id: "other-mod", version: "0.1.0" }, hooks: {} };` },
- };
- const engine = createEngine({ bundle, enabledMods: ["sample-game-mod"] });
- await engine.start();
- try {
- expect(engine.mods.map((m) => m.id)).toEqual(["sample-game-mod"]);
- engine.tick(480);
- expect(engine.world.get("mod:sample-stats")?.greetings).toBe(1);
- } finally {
- await engine.stop();
- }
- });
- });
|