import { describe, expectTypeOf, test } from "vitest"; import type { HotbarState, MapStreamDefinition, RpgActionInput, RpgRoomDescriptor } from "@rpgjs/common"; import type { FactoryProvider as SigneFactoryProvider } from "@signe/di"; import type { NodeConnection, NodeRoom } from "@signe/room/node"; import { provideServerMapStreaming, provideServerRooms, RpgGameplayRoom } from "@rpgjs/server"; import type { RpgEvent, RpgEventHooks, RpgMap, RpgMapHooks, RpgPlayer, RpgPlayerHooks, RpgPlayerConnectionContext, RpgPlayerSaveResult, RpgPlayerSlotLoadResult, RpgPlayerSnapshot, RpgPlayerSnapshotLoadResult, RpgServerEngine, RpgServerEngineHooks, RpgServerStepMetrics, StateData, ActorData, ServerMapStreamingAdapter, } from "@rpgjs/server"; import { createMemoryNodeRoomStorage, createSqliteNodeRoomStorage, type RpgHostedRoom, type RpgHostedRoomConnection, type RpgMemoryRoomStorageProvider, type RpgRoomStorageProvider, } from "@rpgjs/server/node"; import type { RpgProvider, RpgWritableSignal } from "@rpgjs/common"; describe("server public API types", () => { test("gameplay room subclasses are accepted by the room provider", () => { class BattleRoom extends RpgGameplayRoom<{ turn: number }> {} expectTypeOf(provideServerRooms([BattleRoom])).toMatchTypeOf(); }); test("player hooks match the runtime contracts", () => { const hooks = { async onLoad(player, snapshot) { expectTypeOf(player).toEqualTypeOf(); expectTypeOf(snapshot).toEqualTypeOf(); }, onSave(player, snapshot) { expectTypeOf(player).toEqualTypeOf(); expectTypeOf(snapshot).toEqualTypeOf(); }, onInput(player, input) { expectTypeOf(player).toEqualTypeOf(); expectTypeOf(input).toEqualTypeOf>(); }, onAccepted(player, context) { expectTypeOf(player).toEqualTypeOf(); expectTypeOf(context).toEqualTypeOf(); expectTypeOf(context.query).toEqualTypeOf>>(); }, onHotbarChange(player, change) { expectTypeOf(player).toEqualTypeOf(); expectTypeOf(change.state).toEqualTypeOf(); expectTypeOf(change.action).toEqualTypeOf<"initialize" | "assign" | "clear">(); }, canChangeMap(player, nextMap) { expectTypeOf(player).toEqualTypeOf(); expectTypeOf(nextMap).toEqualTypeOf<{ id: string }>(); return nextMap.id !== "forbidden"; }, canChangeRoom(player, room) { expectTypeOf(player).toEqualTypeOf(); expectTypeOf(room).toEqualTypeOf(); return room.kind !== "forbidden"; }, } satisfies RpgPlayerHooks; expectTypeOf(hooks).toMatchTypeOf(); }); test("save and load overloads expose discriminated results", () => { const assertions = (player: RpgPlayer) => { expectTypeOf(player.snapshot()).toEqualTypeOf(); expectTypeOf(player.applySnapshot({ name: "Hero" })).toEqualTypeOf>(); expectTypeOf(player.save()).toEqualTypeOf>(); expectTypeOf(player.save(0)).toEqualTypeOf>(); expectTypeOf(player.load(0)).toEqualTypeOf>(); expectTypeOf(player.load({ name: "Hero" })).toEqualTypeOf>(); expectTypeOf(player.getVariable("quest")).toEqualTypeOf(); expectTypeOf(player.getVariable("quest")).toEqualTypeOf(); expectTypeOf(player.on<{ text: string }>("chat", payload => { void payload.text })).toEqualTypeOf(); expectTypeOf(player.getState("poison")).toEqualTypeOf(); expectTypeOf(player.getHotbar()).toEqualTypeOf(); expectTypeOf(player.assignHotbarSlot(0, { type: "skill", id: "fire" })) .toEqualTypeOf(); expectTypeOf(player.clearHotbarSlot(0)).toEqualTypeOf(); expectTypeOf(player.changeRoom({ kind: "battle", params: { id: "one" } })) .toEqualTypeOf>(); expectTypeOf(player.getCurrentRoom>()) .toEqualTypeOf | null>(); expectTypeOf(player.showCharacterSelect([{ id: "hero", name: "Hero" }])) .toEqualTypeOf>(); expectTypeOf(player.setActor({ id: "hero", name: "Hero" })) .toEqualTypeOf(); expectTypeOf(player.changeActor({ id: "mage", name: "Mage", animations: { attack: "magic" }, class: { id: "caster", name: "Caster" } })) .toEqualTypeOf(); }; expectTypeOf(assertions).toBeFunction(); }); test("event and map hooks expose their complete arguments", () => { const eventHooks = { onAction(event, player, input) { expectTypeOf(event).toEqualTypeOf(); expectTypeOf(player).toEqualTypeOf(); expectTypeOf(input).toEqualTypeOf>(); }, onTouch(event, other, context) { expectTypeOf(event).toEqualTypeOf(); expectTypeOf(other).toEqualTypeOf(); expectTypeOf(context.map).toEqualTypeOf(); }, } satisfies RpgEventHooks; const mapHooks = { onLoad(map) { expectTypeOf(map).toEqualTypeOf(); }, onBeforeUpdate(data, map) { expectTypeOf(data).toEqualTypeOf(); expectTypeOf(map).toEqualTypeOf(); }, } satisfies RpgMapHooks; expectTypeOf(eventHooks).toMatchTypeOf(); expectTypeOf(mapHooks).toMatchTypeOf(); }); test("unknown hook names remain rejected", () => { const hooks: RpgPlayerHooks = { // @ts-expect-error typo in a built-in hook onLoadd() {}, }; expectTypeOf(hooks).toEqualTypeOf(); }); test("map streaming adapters keep provider-specific data typed", () => { type PrivateMap = { source: string }; type ManifestData = { theme: string }; type ChunkData = { tiles: number[] }; const definition = {} as MapStreamDefinition; const adapter = { compile(mapData, map) { expectTypeOf(mapData).toEqualTypeOf(); expectTypeOf(map).toEqualTypeOf(); return definition; }, } satisfies ServerMapStreamingAdapter; expectTypeOf(provideServerMapStreaming(adapter)).toMatchTypeOf(); }); test("gameplay signals and room storage stay RPGJS-owned", () => { const assertions = (player: RpgPlayer, room: RpgHostedRoom) => { expectTypeOf(player.hpSignal).toEqualTypeOf>(); expectTypeOf(room.storage.get("state")).toEqualTypeOf>(); }; expectTypeOf(createMemoryNodeRoomStorage()).toEqualTypeOf(); expectTypeOf(createSqliteNodeRoomStorage({ databasePath: "rooms.sqlite" })) .toEqualTypeOf(); expectTypeOf().toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); expectTypeOf(assertions).toBeFunction(); }); test("SQLite storage requires exactly one database source", () => { const assertSqliteSources = () => { createSqliteNodeRoomStorage({ databasePath: "rooms.sqlite" }); // @ts-expect-error SQLite storage requires database or databasePath createSqliteNodeRoomStorage({}); const database = { exec() {}, prepare() { return { get: () => undefined, run: () => ({ changes: 0 }), all: () => [], }; }, }; createSqliteNodeRoomStorage({ database }); // @ts-expect-error database and databasePath are mutually exclusive createSqliteNodeRoomStorage({ database, databasePath: "rooms.sqlite" }); }; expectTypeOf(assertSqliteSources).toBeFunction(); }); test("Signe providers remain structurally accepted", () => { const provider = { provide: "feature", useFactory: () => ({ enabled: true }), } satisfies RpgProvider; expectTypeOf(provider).toMatchTypeOf(); expectTypeOf().toMatchTypeOf(); }); test("the server engine rejects unknown inherited members", () => { const assertions = (server: RpgServerEngine) => { expectTypeOf(server.onStart()).toEqualTypeOf>(); expectTypeOf(server.getCurrentRoomId()).toEqualTypeOf(); // @ts-expect-error typo in the public server surface server.getCurentRoom(); }; expectTypeOf(assertions).toBeFunction(); }); test("server step hooks expose metrics without breaking one-argument handlers", () => { const hooks = { onStep(server, metrics) { expectTypeOf(server).toEqualTypeOf(); expectTypeOf(metrics).toEqualTypeOf(); }, } satisfies RpgServerEngineHooks; const legacyHooks = { onStep(server) { expectTypeOf(server).toEqualTypeOf(); }, } satisfies RpgServerEngineHooks; expectTypeOf(hooks).toMatchTypeOf(); expectTypeOf(legacyHooks).toMatchTypeOf(); }); test("legacy Signe root re-exports are not part of the stable API", () => { // @ts-expect-error reactive factories are no longer exported by @rpgjs/server type LegacyServerSignal = typeof import("@rpgjs/server")["signal"]; expectTypeOf(); }); });