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