fix: persist admin runtime constants to DB across restarts
Constants were purely in-memory; any server restart reverted them to code defaults. Now saved to metadata table as JSON on every autosave and restored on startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,15 @@ export function createRuntimeConstants() {
|
||||
getDefaults(): TunableConstants {
|
||||
return { ...DEFAULTS };
|
||||
},
|
||||
|
||||
/** Load saved overrides, ignoring any unknown keys */
|
||||
loadOverrides(overrides: Partial<TunableConstants>): void {
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
if (key in DEFAULTS && typeof value === 'number') {
|
||||
current[key as TunableKey] = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export class GameLoop {
|
||||
obstacles: new Set(this.map.getObstacles().map(p => `${p.x},${p.y}`)),
|
||||
foodPositions: this.map.getPointsOfInterest('food').map(p => p.position),
|
||||
});
|
||||
this.saveManager.saveEntityState(this.world, 0);
|
||||
this.saveManager.saveEntityState(this.world, 0, this.runtimeConstants);
|
||||
this.wirePersistenceCallbacks();
|
||||
}
|
||||
console.log('Created new world');
|
||||
@@ -127,7 +127,7 @@ export class GameLoop {
|
||||
this.world.setSingleton('recipeRegistry', RecipeRegistry.createDefault());
|
||||
|
||||
// Load entities (also restores bondRegistry singleton)
|
||||
this.tick = this.saveManager!.loadEntityState(this.world);
|
||||
this.tick = this.saveManager!.loadEntityState(this.world, this.runtimeConstants);
|
||||
|
||||
// Create and load inventionTimeline
|
||||
const inventionTimeline = createInventionTimeline();
|
||||
@@ -273,7 +273,7 @@ export class GameLoop {
|
||||
this.saveInterval = setInterval(() => {
|
||||
if (this.saveManager) {
|
||||
try {
|
||||
this.saveManager.saveEntityState(this.world, this.tick);
|
||||
this.saveManager.saveEntityState(this.world, this.tick, this.runtimeConstants);
|
||||
this.logService.log('info', 'Save', `Autosaved at tick ${this.tick}`);
|
||||
} catch (err) {
|
||||
this.logService.log('error', 'Save', `Autosave failed: ${(err as Error).message}`);
|
||||
@@ -294,7 +294,7 @@ export class GameLoop {
|
||||
}
|
||||
// Final save on shutdown
|
||||
if (this.saveManager) {
|
||||
this.saveManager.saveEntityState(this.world, this.tick);
|
||||
this.saveManager.saveEntityState(this.world, this.tick, this.runtimeConstants);
|
||||
this.saveManager.close();
|
||||
console.log(`Final save at tick ${this.tick}`);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import fs from 'fs';
|
||||
import { openDatabase, closeDatabase, getDatabase, getSchemaVersion, CURRENT_SCHEMA_VERSION } from './database.js';
|
||||
import { saveTiles, loadTiles, saveMetadata, loadMetadata, type TileData } from './worldSerializer.js';
|
||||
import { saveTiles, loadTiles, saveMetadata, loadMetadata, saveRuntimeConstants, loadRuntimeConstants, type TileData } from './worldSerializer.js';
|
||||
import { saveEntities, loadEntities } from './entitySerializer.js';
|
||||
import type { World } from '../ecs/World.js';
|
||||
import type { GameMap } from '../map/GameMap.js';
|
||||
import type { RuntimeConstants } from '../config/runtimeConstants.js';
|
||||
|
||||
export class SaveManager {
|
||||
private filepath: string;
|
||||
@@ -92,16 +93,25 @@ export class SaveManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Save all entity state (entities, components, relationships, bonds) + metadata */
|
||||
saveEntityState(world: World, tick: number): void {
|
||||
/** Save all entity state (entities, components, relationships, bonds) + metadata + runtime constants */
|
||||
saveEntityState(world: World, tick: number, runtimeConstants?: RuntimeConstants): void {
|
||||
saveEntities(world);
|
||||
saveMetadata({ tick, lastSavedAt: new Date().toISOString() });
|
||||
if (runtimeConstants) {
|
||||
saveRuntimeConstants(runtimeConstants.getAll());
|
||||
}
|
||||
}
|
||||
|
||||
/** Load entity state into world. Returns saved tick number. */
|
||||
loadEntityState(world: World): number {
|
||||
/** Load entity state into world. Restores runtime constants if saved. Returns saved tick number. */
|
||||
loadEntityState(world: World, runtimeConstants?: RuntimeConstants): number {
|
||||
loadEntities(world);
|
||||
const meta = loadMetadata();
|
||||
if (runtimeConstants) {
|
||||
const saved = loadRuntimeConstants();
|
||||
if (saved) {
|
||||
runtimeConstants.loadOverrides(saved);
|
||||
}
|
||||
}
|
||||
return meta.tick;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,3 +114,22 @@ export function loadMetadata(): { tick: number; lastSavedAt: string } {
|
||||
|
||||
return { tick, lastSavedAt };
|
||||
}
|
||||
|
||||
export function saveRuntimeConstants(constants: Record<string, number>): void {
|
||||
const db = getDatabase();
|
||||
const upsert = db.prepare(
|
||||
"INSERT INTO metadata (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
|
||||
);
|
||||
upsert.run('runtime_constants', JSON.stringify(constants));
|
||||
}
|
||||
|
||||
export function loadRuntimeConstants(): Record<string, number> | null {
|
||||
const db = getDatabase();
|
||||
const row = db.prepare("SELECT value FROM metadata WHERE key = 'runtime_constants'").get() as { value: string } | undefined;
|
||||
if (!row) return null;
|
||||
try {
|
||||
return JSON.parse(row.value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user