From d4a8668b54df9f45c5e58c575a542f9afad17313 Mon Sep 17 00:00:00 2001 From: hypercross Date: Thu, 2 Apr 2026 10:26:42 +0800 Subject: [PATCH] refactor: remove context. add game. --- src/core/context.ts | 228 -------------------------------------------- src/core/game.ts | 27 ++++++ 2 files changed, 27 insertions(+), 228 deletions(-) delete mode 100644 src/core/context.ts create mode 100644 src/core/game.ts diff --git a/src/core/context.ts b/src/core/context.ts deleted file mode 100644 index a667555..0000000 --- a/src/core/context.ts +++ /dev/null @@ -1,228 +0,0 @@ -import {createModel, Signal, signal} from '@preact/signals-core'; -import {createEntityCollection} from "../utils/entity"; -import {Part} from "./part"; -import {Region} from "./region"; -import {createCommandRunnerContext, CommandRegistry, createCommandRegistry, type CommandRunnerContextExport} from "../utils/command"; -import type {Command} from "../utils/command"; -import {parseCommand} from "../utils/command/command-parse"; -import {applyCommandSchema} from "../utils/command/command-validate"; -import {parseCommandSchema} from "../utils/command/schema-parse"; -import type {CommandRunner} from "../utils/command/command-runner"; - -export type Context = { - type: string; -} - -export type GameQueueState = 'idle' | 'processing' | 'waiting-for-prompt'; - -export interface IGameContext { - parts: ReturnType>; - regions: ReturnType>; - commandRegistry: Signal>; - contexts: Signal[]>; - pushContext: (context: Context) => Context; - popContext: () => void; - latestContext: (type: T['type']) => Signal | undefined; - registerCommand: (name: string, runner: CommandRunner) => void; - unregisterCommand: (name: string) => void; - enqueue: (input: string) => void; - enqueueAll: (inputs: string[]) => void; - dispatchCommand: (input: string) => void; -} - -export const GameContext = createModel((root: Context) => { - const parts = createEntityCollection(); - const regions = createEntityCollection(); - const commandRegistry = signal>(createCommandRegistry()); - const contexts = signal[]>([]); - contexts.value = [signal(root)]; - - const inputQueue: string[] = []; - let processing = false; - let pendingPromptResolve: ((cmd: Command) => void) | null = null; - let pendingPromptReject: ((err: Error) => void) | null = null; - let activeRunnerCtx: CommandRunnerContextExport | null = null; - - function pushContext(context: Context) { - const ctxSignal = signal(context); - contexts.value = [...contexts.value, ctxSignal]; - return context; - } - - function popContext() { - if (contexts.value.length > 1) { - contexts.value = contexts.value.slice(0, -1); - } - } - - function latestContext(type: T['type']): Signal | undefined { - for(let i = contexts.value.length - 1; i >= 0; i--){ - if(contexts.value[i].value.type === type){ - return contexts.value[i] as Signal; - } - } - return undefined; - } - - function registerCommand(name: string, runner: CommandRunner) { - const newRegistry = new Map(commandRegistry.value); - newRegistry.set(name, runner); - commandRegistry.value = newRegistry; - } - - function unregisterCommand(name: string) { - const newRegistry = new Map(commandRegistry.value); - newRegistry.delete(name); - commandRegistry.value = newRegistry; - } - - function makeRunnerCtx(): CommandRunnerContextExport { - const ctx = createCommandRunnerContext(commandRegistry.value, instance as IGameContext); - - ctx.prompt = async (schema) => { - const parsedSchema = typeof schema === 'string' - ? parseCommandSchema(schema) - : schema; - return new Promise((resolve, reject) => { - pendingPromptResolve = resolve; - pendingPromptReject = reject; - const event = { schema: parsedSchema, resolve, reject }; - for (const listener of (ctx as any)._listeners || []) { - listener(event); - } - }); - }; - - const origRun = ctx.run.bind(ctx); - ctx.run = async (input: string) => { - const prevCtx = activeRunnerCtx; - activeRunnerCtx = ctx; - const result = await runCommand(ctx, input); - activeRunnerCtx = prevCtx; - return result; - }; - - return ctx; - } - - async function runCommand(runnerCtx: CommandRunnerContextExport, input: string): Promise<{ success: true; result: unknown } | { success: false; error: string }> { - const command = parseCommand(input); - const runner = runnerCtx.registry.get(command.name); - if (!runner) { - return { success: false, error: `Unknown command: ${command.name}` }; - } - - const validationResult = applyCommandSchema(command, runner.schema); - if (!validationResult.valid) { - return { success: false, error: validationResult.errors.join('; ') }; - } - - try { - const result = await runner.run.call(runnerCtx, validationResult.command); - return { success: true, result }; - } catch (e) { - const error = e as Error; - return { success: false, error: error.message }; - } - } - - async function processQueue(): Promise { - if (processing) return; - processing = true; - - while (inputQueue.length > 0) { - if (pendingPromptResolve) { - const input = inputQueue.shift()!; - try { - const command = parseCommand(input); - pendingPromptResolve(command); - } catch (e) { - pendingPromptReject!(new Error(`Invalid input for prompt: ${input}`)); - } - pendingPromptResolve = null; - pendingPromptReject = null; - continue; - } - - const input = inputQueue.shift()!; - const runnerCtx = makeRunnerCtx(); - const prevCtx = activeRunnerCtx; - activeRunnerCtx = runnerCtx; - - runCommand(runnerCtx, input).finally(() => { - if (activeRunnerCtx === runnerCtx) { - activeRunnerCtx = prevCtx; - } - }); - - await Promise.resolve(); - } - - processing = false; - } - - function enqueue(input: string) { - if (pendingPromptResolve) { - try { - const command = parseCommand(input); - pendingPromptResolve(command); - } catch (e) { - pendingPromptReject!(new Error(`Invalid input for prompt: ${input}`)); - } - pendingPromptResolve = null; - pendingPromptReject = null; - } else { - inputQueue.push(input); - if (!processing) { - void processQueue(); - } - } - } - - function enqueueAll(inputs: string[]) { - for (const input of inputs) { - if (pendingPromptResolve) { - try { - const command = parseCommand(input); - pendingPromptResolve(command); - } catch (e) { - pendingPromptReject!(new Error(`Invalid input for prompt: ${input}`)); - } - pendingPromptResolve = null; - pendingPromptReject = null; - } else { - inputQueue.push(input); - } - } - if (!processing) { - void processQueue(); - } - } - - function dispatchCommand(input: string) { - enqueue(input); - } - - const instance: IGameContext = { - parts, - regions, - commandRegistry, - contexts, - pushContext, - popContext, - latestContext, - registerCommand, - unregisterCommand, - enqueue, - enqueueAll, - dispatchCommand, - }; - - return instance; -}) - -export function createGameContext(root: Context = { type: 'game' }) { - return new GameContext(root); -} - -export type GameContextInstance = IGameContext; diff --git a/src/core/game.ts b/src/core/game.ts new file mode 100644 index 0000000..211dcc9 --- /dev/null +++ b/src/core/game.ts @@ -0,0 +1,27 @@ +import {createEntityCollection} from "../utils/entity"; +import {Part} from "./part"; +import {Region} from "./region"; +import {CommandRegistry, CommandRunnerContextExport, createCommandRunnerContext, PromptEvent} from "../utils/command"; +import {AsyncQueue} from "../utils/async-queue"; + +export interface IGameContext { + parts: ReturnType>; + regions: ReturnType>; + commands: CommandRunnerContextExport; + inputs: AsyncQueue; +} + +export function createGameContext(commandRegistry: CommandRegistry) { + const parts = createEntityCollection(); + const regions = createEntityCollection(); + const ctx: IGameContext = { + parts, + regions, + commands: null, + inputs: new AsyncQueue(), + }; + ctx.commands = createCommandRunnerContext(commandRegistry, ctx); + ctx.commands.on('prompt', (prompt: PromptEvent) => ctx.inputs.push(prompt)); + + return ctx; +} \ No newline at end of file