refactor: remove context. add game.
This commit is contained in:
parent
9c7baa29ef
commit
d4a8668b54
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue