Spaces:
Sleeping
Sleeping
import { ConvexError, v } from 'convex/values'; | |
import { DatabaseReader, MutationCtx, internalAction, mutation, query } from '../_generated/server'; | |
import { insertInput } from './insertInput'; | |
import { Game } from './game'; | |
import { internal } from '../_generated/api'; | |
import { sleep } from '../util/sleep'; | |
import { Id } from '../_generated/dataModel'; | |
import { ENGINE_ACTION_DURATION } from '../constants'; | |
export async function createEngine(ctx: MutationCtx) { | |
const now = Date.now(); | |
const engineId = await ctx.db.insert('engines', { | |
currentTime: now, | |
generationNumber: 0, | |
running: true, | |
}); | |
return engineId; | |
} | |
async function loadWorldStatus(db: DatabaseReader, worldId: Id<'worlds'>) { | |
const worldStatus = await db | |
.query('worldStatus') | |
.withIndex('worldId', (q) => q.eq('worldId', worldId)) | |
.unique(); | |
if (!worldStatus) { | |
throw new Error(`No engine found for world ${worldId}`); | |
} | |
return worldStatus; | |
} | |
export async function startEngine(ctx: MutationCtx, worldId: Id<'worlds'>) { | |
const { engineId } = await loadWorldStatus(ctx.db, worldId); | |
const engine = await ctx.db.get(engineId); | |
if (!engine) { | |
throw new Error(`Invalid engine ID: ${engineId}`); | |
} | |
if (engine.running) { | |
throw new Error(`Engine ${engineId} isn't currently stopped`); | |
} | |
const now = Date.now(); | |
const generationNumber = engine.generationNumber + 1; | |
await ctx.db.patch(engineId, { | |
// Forcibly advance time to the present. This does mean we'll skip | |
// simulating the time the engine was stopped, but we don't want | |
// to have to simulate a potentially large stopped window and send | |
// it down to clients. | |
lastStepTs: engine.currentTime, | |
currentTime: now, | |
running: true, | |
generationNumber, | |
}); | |
await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, { | |
worldId: worldId, | |
generationNumber, | |
maxDuration: ENGINE_ACTION_DURATION, | |
}); | |
} | |
export async function kickEngine(ctx: MutationCtx, worldId: Id<'worlds'>) { | |
const { engineId } = await loadWorldStatus(ctx.db, worldId); | |
const engine = await ctx.db.get(engineId); | |
if (!engine) { | |
throw new Error(`Invalid engine ID: ${engineId}`); | |
} | |
if (!engine.running) { | |
throw new Error(`Engine ${engineId} isn't currently running`); | |
} | |
const generationNumber = engine.generationNumber + 1; | |
await ctx.db.patch(engineId, { generationNumber }); | |
await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, { | |
worldId: worldId, | |
generationNumber, | |
maxDuration: ENGINE_ACTION_DURATION, | |
}); | |
} | |
export async function stopEngine(ctx: MutationCtx, worldId: Id<'worlds'>) { | |
const { engineId } = await loadWorldStatus(ctx.db, worldId); | |
const engine = await ctx.db.get(engineId); | |
if (!engine) { | |
throw new Error(`Invalid engine ID: ${engineId}`); | |
} | |
if (!engine.running) { | |
throw new Error(`Engine ${engineId} isn't currently running`); | |
} | |
await ctx.db.patch(engineId, { running: false }); | |
} | |
export const runStep = internalAction({ | |
args: { | |
worldId: v.id('worlds'), | |
generationNumber: v.number(), | |
maxDuration: v.number(), | |
}, | |
handler: async (ctx, args) => { | |
try { | |
const { engine, gameState } = await ctx.runQuery(internal.aiTown.game.loadWorld, { | |
worldId: args.worldId, | |
generationNumber: args.generationNumber, | |
}); | |
const game = new Game(engine, args.worldId, gameState); | |
let now = Date.now(); | |
const deadline = now + args.maxDuration; | |
while (now < deadline) { | |
await game.runStep(ctx, now); | |
const sleepUntil = Math.min(now + game.stepDuration, deadline); | |
await sleep(sleepUntil - now); | |
now = Date.now(); | |
} | |
await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, { | |
worldId: args.worldId, | |
generationNumber: game.engine.generationNumber, | |
maxDuration: args.maxDuration, | |
}); | |
} catch (e: unknown) { | |
if (e instanceof ConvexError) { | |
if (e.data.kind === 'engineNotRunning') { | |
console.debug(`Engine is not running: ${e.message}`); | |
return; | |
} | |
if (e.data.kind === 'generationNumber') { | |
console.debug(`Generation number mismatch: ${e.message}`); | |
return; | |
} | |
} | |
throw e; | |
} | |
}, | |
}); | |
export const sendInput = mutation({ | |
args: { | |
worldId: v.id('worlds'), | |
name: v.string(), | |
args: v.any(), | |
}, | |
handler: async (ctx, args) => { | |
return await insertInput(ctx, args.worldId, args.name as any, args.args); | |
}, | |
}); | |
export const inputStatus = query({ | |
args: { | |
inputId: v.id('inputs'), | |
}, | |
handler: async (ctx, args) => { | |
const input = await ctx.db.get(args.inputId); | |
if (!input) { | |
throw new Error(`Invalid input ID: ${args.inputId}`); | |
} | |
return input.returnValue ?? null; | |
}, | |
}); | |