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:
root
2026-03-10 19:37:17 +00:00
parent 53be21e512
commit 3721d2de5b
4 changed files with 47 additions and 9 deletions
+9
View File
@@ -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;
}
}
},
};
}
+4 -4
View File
@@ -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}`);
}
+15 -5
View File
@@ -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;
}
+19
View File
@@ -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;
}
}