Files
AzerothAI/core/store.ts
2026-09-24 03:16:52 +12:00

90 lines
4.3 KiB
TypeScript

import { env } from 'cloudflare:workers';
import { splitState } from './state-chunks';
import { seed } from './seed';
import type { State,StateStore,Bot,Recipe,Home } from './types';
function migrate(raw:any):State{
const defaults=seed();
const recipes:Recipe[]=(raw.recipes??defaults.recipes).map((r:Recipe)=>r);
for(const recipe of defaults.recipes){
if(!recipes.some(r=>r.id===recipe.id))recipes.push(structuredClone(recipe));
}
const homes:Home[]=(raw.homes??defaults.homes).map((h:Home)=>h);
for(const home of defaults.homes){
if(!homes.some(h=>h.id===home.id))homes.push(structuredClone(home));
}
const bots:Bot[]=(raw.bots??[]).map((b:any)=>{
const seeded=defaults.bots.find(x=>x.id===b.id);
const home=homes.find(h=>h.residentIds.includes(b.id));
return {
...b,
tier:b.tier??seeded?.tier??'resident',
mortal:b.mortal??true,
plotArmorHpFloor:b.plotArmorHpFloor??seeded?.plotArmorHpFloor??0,
simulationBand:b.simulationBand??seeded?.simulationBand??(b.status==='online'?'background':'dormant'),
homeId:b.homeId??seeded?.homeId??home?.id,
routine:b.routine??seeded?.routine??{wakeHour:7,sleepHour:23,preferredZones:[b.zone].filter(Boolean)},
autonomy:{managed:b.autonomy?.managed??false,...(b.autonomy??{})}
};
});
for(const hero of defaults.bots.filter(b=>b.tier==='hero')){
if(!bots.some(b=>b.id===hero.id||b.name.toLowerCase()===hero.name.toLowerCase()))bots.push(structuredClone(hero));
}
for(const home of homes){
home.residentIds=home.residentIds.filter(id=>bots.some(b=>b.id===id));
}
for(const bot of bots){
if(bot.homeId){
const home=homes.find(h=>h.id===bot.homeId);
if(home&&!home.residentIds.includes(bot.id)&&home.residentIds.length<home.capacity)home.residentIds.push(bot.id);
}
}
return {
...defaults,
...raw,
bots,
homes,
recipes,
events:raw.events??defaults.events,
sequence:raw.sequence??defaults.sequence,
settings:{
...defaults.settings,
...(raw.settings??{}),
hardcore:{...defaults.settings.hardcore,...(raw.settings?.hardcore??{})},
population:{...defaults.settings.population,...(raw.settings?.population??{})},
brains:{...defaults.settings.brains,...(raw.settings?.brains??{})}
},
director:{...defaults.director,...(raw.director??{})},
population:{...defaults.population,...(raw.population??{})},
operations:raw.operations??{}
};
}
export class D1Store implements StateStore {
async load():Promise<State>{
if(!env.DB) throw new Error('Realm database unavailable');
const initial=seed();
await env.DB.prepare('INSERT OR IGNORE INTO realm_state (id,revision,payload) VALUES (?,?,?)').bind('realm',0,JSON.stringify(initial)).run();
await this.chunks();
const result=await env.DB.prepare("SELECT s.payload AS manifest,c.payload AS chunk FROM realm_state s LEFT JOIN realm_state_chunks c ON c.token=json_extract(s.payload,'$.chunkToken') WHERE s.id=? ORDER BY c.part").bind('realm').all<{manifest:string;chunk:string|null}>();
const rows=result.results;if(!rows.length)throw new Error('Realm state unavailable');
const manifest=JSON.parse(rows[0].manifest);
if(manifest.chunkToken){if(rows.length!==manifest.parts||rows.some(r=>r.chunk===null))throw new Error('Realm state chunks incomplete');return migrate(JSON.parse(rows.map(r=>r.chunk).join('')));}
return migrate(manifest);
}
private async chunks(){await env.DB!.prepare('CREATE TABLE IF NOT EXISTS realm_state_chunks (token TEXT,part INTEGER,payload TEXT,PRIMARY KEY(token,part))').run();}
async save(s:State,expected:number){
await this.chunks();s.revision=expected+1;
const token=crypto.randomUUID(),parts=splitState(s),manifest=JSON.stringify({chunkToken:token,parts:parts.length});
const statements=[env.DB!.prepare('UPDATE realm_state SET revision=?,payload=? WHERE id=? AND revision=?').bind(s.revision,manifest,'realm',expected),
...parts.map((part,i)=>env.DB!.prepare('INSERT INTO realm_state_chunks(token,part,payload) SELECT ?,?,? WHERE EXISTS (SELECT 1 FROM realm_state WHERE id=? AND payload=?)').bind(token,i,part,'realm',manifest)),
env.DB!.prepare('DELETE FROM realm_state_chunks WHERE token<>? AND EXISTS (SELECT 1 FROM realm_state WHERE id=? AND payload=?)').bind(token,'realm',manifest)];
const result=await env.DB!.batch(statements);
if(result[0].meta.changes!==1)throw new Error('State changed in another request. Refresh and try again.');
}
}