feat: v0.7.0 — server-authoritative sim engine: tick-on-read clock (time-scale + pause via POST /api/sim), contract expiry + market spawn, analytic plane movement/finalization

This commit is contained in:
2026-09-17 12:38:52 +00:00
parent 7fdc43bc7e
commit 96835e935d
22 changed files with 1393 additions and 55 deletions
+28 -2
View File
@@ -29,7 +29,8 @@ CREATE TABLE IF NOT EXISTS planes (
location_icao TEXT NOT NULL,
contract_id TEXT,
lat REAL NOT NULL,
lon REAL NOT NULL
lon REAL NOT NULL,
departure_sim_time INTEGER
);
CREATE TABLE IF NOT EXISTS contracts (
id TEXT PRIMARY KEY NOT NULL,
@@ -50,10 +51,25 @@ CREATE TABLE IF NOT EXISTS account (
CREATE TABLE IF NOT EXISTS sim_state (
clock_ms INTEGER NOT NULL,
time_scale REAL NOT NULL,
paused INTEGER NOT NULL
paused INTEGER NOT NULL,
last_real_tick_ms INTEGER NOT NULL DEFAULT 0
);
`;
// v0.7.0: DBs created by earlier versions lack the new columns; CREATE TABLE IF
// NOT EXISTS won't add them, so ALTER (guarded by PRAGMA table_info). Fresh DBs
// already have everything from DDL above and this is a no-op for them.
function migrate(raw: RawSqlJsDatabase): void {
const cols = (table: string): Set<string> =>
new Set(raw.exec(`PRAGMA table_info(${table})`)[0].values.map((r) => String(r[1])));
if (!cols('sim_state').has('last_real_tick_ms')) {
raw.exec('ALTER TABLE sim_state ADD COLUMN last_real_tick_ms INTEGER NOT NULL DEFAULT 0');
}
if (!cols('planes').has('departure_sim_time')) {
raw.exec('ALTER TABLE planes ADD COLUMN departure_sim_time INTEGER');
}
}
// FLIGHT_DB_PATH override exists so unit tests can point at a temp file instead
// of the real ./db/flight.db.
export function defaultDbPath(): string {
@@ -70,6 +86,7 @@ async function open(file: string): Promise<GameDb> {
}
const raw = bytes ? new SQL.Database(new Uint8Array(bytes)) : new SQL.Database();
raw.exec(DDL);
migrate(raw);
const instance: GameDb = { db: drizzle(raw, { schema }), raw, file };
@@ -78,6 +95,15 @@ async function open(file: string): Promise<GameDb> {
if (Number(res[0]?.values?.[0]?.[0] ?? 0) === 0) {
await seedEmpty(instance);
}
// v0.7.0 fixup: pre-v0.7.0 DBs got last_real_tick_ms = 0 from the ALTER TABLE
// default. Reset it to "now" so the first tick-on-read resumes cleanly instead
// of taking one clamped catch-up jump. Fresh seeds already carry a real value.
const simRows = raw.exec('SELECT clock_ms, last_real_tick_ms FROM sim_state');
if (simRows[0]?.values?.length && Number(simRows[0].values[0][1]) === 0) {
raw.exec(`UPDATE sim_state SET last_real_tick_ms = ${Date.now()}`);
await persist(instance);
}
return instance;
}