refactor: remove context. add game.

This commit is contained in:
hypercross 2026-04-02 10:26:42 +08:00
parent 9c7baa29ef
commit d4a8668b54
2 changed files with 27 additions and 228 deletions

View File

@ -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<typeof createEntityCollection<Part>>;
regions: ReturnType<typeof createEntityCollection<Region>>;
commandRegistry: Signal<CommandRegistry<IGameContext>>;
contexts: Signal<Signal<Context>[]>;
pushContext: (context: Context) => Context;
popContext: () => void;
latestContext: <T extends Context>(type: T['type']) => Signal<T> | undefined;
registerCommand: (name: string, runner: CommandRunner<IGameContext, unknown>) => 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<Part>();
const regions = createEntityCollection<Region>();
const commandRegistry = signal<CommandRegistry<IGameContext>>(createCommandRegistry());
const contexts = signal<Signal<Context>[]>([]);
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<IGameContext> | 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<T extends Context>(type: T['type']): Signal<T> | undefined {
for(let i = contexts.value.length - 1; i >= 0; i--){
if(contexts.value[i].value.type === type){
return contexts.value[i] as Signal<T>;
}
}
return undefined;
}
function registerCommand(name: string, runner: CommandRunner<IGameContext, unknown>) {
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<IGameContext> {
const ctx = createCommandRunnerContext(commandRegistry.value, instance as IGameContext);
ctx.prompt = async (schema) => {
const parsedSchema = typeof schema === 'string'
? parseCommandSchema(schema)
: schema;
return new Promise<Command>((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<IGameContext>, 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<void> {
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;

27
src/core/game.ts Normal file
View File

@ -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<typeof createEntityCollection<Part>>;
regions: ReturnType<typeof createEntityCollection<Region>>;
commands: CommandRunnerContextExport<IGameContext>;
inputs: AsyncQueue<PromptEvent>;
}
export function createGameContext(commandRegistry: CommandRegistry<IGameContext>) {
const parts = createEntityCollection<Part>();
const regions = createEntityCollection<Region>();
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;
}