db+game: schema_version on saves with ordered idempotent backfill (v2 bodies, v3 crew) — no more DB wipes
This commit is contained in:
+60
-12
@@ -45,20 +45,36 @@ function expansionRng(seed: string, turn: number): () => number {
|
||||
return mulberry32((Number(seed) + turn * 1_000_003) >>> 0)
|
||||
}
|
||||
|
||||
// Inserts one system together with its generated bodies (planets + moons), so a system is
|
||||
// never left without worlds to explore. Bodies derive from (seed, system id) via bodyRng —
|
||||
// independent of generation order and every other system.
|
||||
async function insertSystemWithBodies(db: GameDb, saveId: number, ws: { name: string; x: number; y: number }, seed: string): Promise<number> {
|
||||
const [row] = await db.db.insert(schema.solarSystems).values({ saveId, name: ws.name, x: ws.x, y: ws.y }).returning({ id: schema.solarSystems.id })
|
||||
for (const body of generatePlanets(bodyRng(seed, row.id))) {
|
||||
const [planet] = await db.db.insert(schema.planets).values({ systemId: row.id, parentId: null, name: body.name, type: body.type, hazard: body.hazard }).returning({ id: schema.planets.id })
|
||||
// The bodies of one system (planets + moons), derived from (seed, system id) via bodyRng —
|
||||
// independent of generation order and every other system. Shared by new-game creation and the
|
||||
// v2 backfill so both produce identical rows for a given save+system.
|
||||
async function insertBodiesForSystem(db: GameDb, systemId: number, seed: string): Promise<void> {
|
||||
for (const body of generatePlanets(bodyRng(seed, systemId))) {
|
||||
const [planet] = await db.db.insert(schema.planets).values({ systemId, parentId: null, name: body.name, type: body.type, hazard: body.hazard }).returning({ id: schema.planets.id })
|
||||
for (const moon of body.moons) {
|
||||
await db.db.insert(schema.planets).values({ systemId: row.id, parentId: planet.id, name: moon.name, type: moon.type, hazard: moon.hazard })
|
||||
await db.db.insert(schema.planets).values({ systemId, parentId: planet.id, name: moon.name, type: moon.type, hazard: moon.hazard })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inserts one system together with its generated bodies, so a system is never left without
|
||||
// worlds to explore.
|
||||
async function insertSystemWithBodies(db: GameDb, saveId: number, ws: { name: string; x: number; y: number }, seed: string): Promise<number> {
|
||||
const [row] = await db.db.insert(schema.solarSystems).values({ saveId, name: ws.name, x: ws.x, y: ws.y }).returning({ id: schema.solarSystems.id })
|
||||
await insertBodiesForSystem(db, row.id, seed)
|
||||
return row.id
|
||||
}
|
||||
|
||||
// v2 backfill: every system without planets gets its generated bodies. Idempotent — systems
|
||||
// that already have rows are skipped, so a second run changes nothing.
|
||||
async function ensureBodiesForAllSystems(db: GameDb, saveId: number, seed: string): Promise<void> {
|
||||
const systems = await db.db.select().from(schema.solarSystems)
|
||||
for (const system of systems) {
|
||||
const rows = await db.db.select({ id: schema.planets.id }).from(schema.planets).where(eq(schema.planets.systemId, system.id))
|
||||
if (rows.length === 0) await insertBodiesForSystem(db, system.id, seed)
|
||||
}
|
||||
}
|
||||
|
||||
async function insertRoutes(db: GameDb, edges: [number, number][], resolve: (index: number) => number): Promise<void> {
|
||||
for (const [a, b] of edges) {
|
||||
const from = resolve(a)
|
||||
@@ -75,17 +91,29 @@ async function seedStartingCrew(db: GameDb, saveId: number, seed: string): Promi
|
||||
}
|
||||
}
|
||||
|
||||
// Creates the save + starting region when the DB has none. No-op otherwise — safe to call
|
||||
// on every request (single player, one process: check-then-insert cannot race in practice).
|
||||
// What each schema version added to a save. Saves created by older code lack the data newer
|
||||
// versions seed at new-game time, so ensureSave runs these steps in order for anything below
|
||||
// current — each step is idempotent (a no-op when its data already exists). When #9/#10 add
|
||||
// seeding, bump SCHEMA_VERSION and append a step here.
|
||||
export const SCHEMA_VERSION = 3 // v1: save + start region only · v2: bodies for every system · v3: starting crew
|
||||
|
||||
// Creates the save + starting region when the DB has none; backfills older saves otherwise.
|
||||
// Safe to call on every request (single player, one process: check-then-insert cannot race).
|
||||
export async function ensureSave(db: GameDb): Promise<void> {
|
||||
const existing = await db.db.select().from(schema.saves).limit(1)
|
||||
if (existing.length > 0) return
|
||||
if (existing.length === 0) {
|
||||
await createNewGame(db)
|
||||
return
|
||||
}
|
||||
await backfillSave(db, existing[0])
|
||||
}
|
||||
|
||||
async function createNewGame(db: GameDb): Promise<void> {
|
||||
const seed = newSeed()
|
||||
const region = generateStartRegion(mulberry32(Number(seed)))
|
||||
const nowIso = new Date().toISOString()
|
||||
|
||||
const [save] = await db.db.insert(schema.saves).values({ seed, turn: 0, createdAt: nowIso }).returning()
|
||||
const [save] = await db.db.insert(schema.saves).values({ seed, turn: 0, createdAt: nowIso, schemaVersion: SCHEMA_VERSION }).returning()
|
||||
// All inserted as undiscovered first; the start is then marked discovered + current.
|
||||
const startId = await insertSystemWithBodies(db, save.id, region.start, seed)
|
||||
const neighborIds: number[] = []
|
||||
@@ -100,6 +128,26 @@ export async function ensureSave(db: GameDb): Promise<void> {
|
||||
await persist(db)
|
||||
}
|
||||
|
||||
// Ordered idempotent backfill for a save created by older code. A NULL version means v1.
|
||||
async function backfillSave(db: GameDb, save: { id: number; seed: string; schemaVersion: number | null }): Promise<void> {
|
||||
const from = save.schemaVersion ?? 1
|
||||
let version = from
|
||||
|
||||
if (version < 2) {
|
||||
await ensureBodiesForAllSystems(db, save.id, save.seed) // bodies derive from (seed, system id) — identical to fresh generation
|
||||
version = 2
|
||||
}
|
||||
if (version < 3) {
|
||||
if ((await db.db.select().from(schema.crew)).length === 0) await seedStartingCrew(db, save.id, save.seed)
|
||||
version = 3
|
||||
}
|
||||
|
||||
if (from !== SCHEMA_VERSION) {
|
||||
await db.db.update(schema.saves).set({ schemaVersion: version }).where(eq(schema.saves.id, save.id))
|
||||
await persist(db)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorldState(db: GameDb): Promise<WorldState> {
|
||||
await ensureSave(db)
|
||||
const [save] = await db.db.select().from(schema.saves).limit(1)
|
||||
|
||||
Reference in New Issue
Block a user