90 lines
4.3 KiB
TypeScript
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.');
|
|
}
|
|
}
|