iv: fix background_id not persisting, suppress seeding drift noise
This commit is contained in:
403
server/index.js
403
server/index.js
@@ -1,25 +1,20 @@
|
||||
// OTIVM server — OTIVM-IV
|
||||
// Per-player SQLite integration.
|
||||
// TESSERA world database (data/otivm.sqlite3) — read-only, unchanged.
|
||||
// Player databases (data/saves/{token}.sqlite3) — one per player,
|
||||
// write-safe (single writer), created from data/create_player_db.sql
|
||||
// on first access.
|
||||
// JSON save files (data/saves/{token}.json) — never deleted, migrated
|
||||
// transparently on first SQLite access if present.
|
||||
// Frontend interface unchanged — GET/POST /api/save/:token same as before.
|
||||
//
|
||||
// Change 1 (OTIVM-IV): on dispatch_start event, create ventures +
|
||||
// venture_legs rows. On dispatch_complete, close venture with outcome_net.
|
||||
// Change 2 (OTIVM-IV): on den change, write parameter_drift_log row
|
||||
// (trigger_type = 'venture_complete'). On aut change, write
|
||||
// parameter_drift_log row (trigger_type = 'interval_complete').
|
||||
// Fix 1: if actor_profile exists but background_id = 'unknown' and the
|
||||
// incoming body has a real background_id, patch the profile and seed
|
||||
// actor_parameters from background_starting_values. This handles the
|
||||
// race where the first POST (background selection save) arrives before
|
||||
// the background_id is set in the body.
|
||||
//
|
||||
// Fix 2: drift log entries are suppressed when there is no prior
|
||||
// parameter row (prevDen = null / first write). This eliminates noise
|
||||
// rows from the initial seeding sequence.
|
||||
//
|
||||
// Cost split — placeholder pending a proper cost model:
|
||||
// cost_vectura = 60% of route cost (VECTVRA — freight charge)
|
||||
// cost_portoria = 25% of route cost (PORTORIUM — customs duty)
|
||||
// cost_other = 15% of route cost (horreum, incidentals)
|
||||
// These proportions are fixed constants here, not in the schema.
|
||||
// When a real cost model is designed, this is the only place to change.
|
||||
|
||||
import Fastify from 'fastify'
|
||||
import fastifyStatic from '@fastify/static'
|
||||
@@ -40,14 +35,9 @@ const SCHEMA_PATH = join(ROOT, 'data', 'create_player_db.sql')
|
||||
await mkdir(SAVES_DIR, { recursive: true })
|
||||
|
||||
// ── Cost split constants ─────────────────────────────────────────────────────
|
||||
// Placeholder proportions — change here only when a real cost model is defined.
|
||||
const COST_VECTURA_RATIO = 0.60
|
||||
const COST_PORTORIA_RATIO = 0.25
|
||||
// cost_other = remainder (1 - 0.60 - 0.25 = 0.15)
|
||||
|
||||
// Route mode classification — used for venture_legs.mode
|
||||
// 'olive' and 'wine' are road; 'grain', 'linen' are sea.
|
||||
// Extend this map when new routes are added.
|
||||
const ROUTE_MODE = {
|
||||
olive: 'road',
|
||||
wine: 'road',
|
||||
@@ -55,16 +45,13 @@ const ROUTE_MODE = {
|
||||
linen: 'sea',
|
||||
}
|
||||
|
||||
// Route cargo definitions — used for ventures.cargo_type / cargo_unit
|
||||
const ROUTE_CARGO = {
|
||||
olive: { type: 'Olive oil, Garum', unit: 'amphora' },
|
||||
wine: { type: 'Campanian wine, Wool', unit: 'amphora' },
|
||||
grain: { type: 'Adriatic grain, Amber', unit: 'modius' },
|
||||
linen: { type: 'Berber linen, Frankincense',unit: 'talent' },
|
||||
linen: { type: 'Berber linen, Frankincense', unit: 'talent' },
|
||||
}
|
||||
|
||||
// Route H3 waypoints — origin and destination res-5 cell IDs
|
||||
// Sourced from src/constants.js WAYPOINTS and ROUTES
|
||||
const ROUTE_H3 = {
|
||||
olive: { origin: '851e805bfffffff', destination: '851e8333fffffff' },
|
||||
wine: { origin: '851e8333fffffff', destination: '851e8ba3fffffff' },
|
||||
@@ -72,8 +59,6 @@ const ROUTE_H3 = {
|
||||
linen: { origin: '85386e23fffffff', destination: '853f5ba7fffffff' },
|
||||
}
|
||||
|
||||
// Route cost and profit — sourced from src/constants.js ROUTES
|
||||
// Duplicated here so server/index.js has no dependency on frontend constants.
|
||||
const ROUTE_ECONOMICS = {
|
||||
olive: { cost: 8, profit: 12, duration_ms: 6000 },
|
||||
wine: { cost: 14, profit: 22, duration_ms: 9000 },
|
||||
@@ -81,7 +66,6 @@ const ROUTE_ECONOMICS = {
|
||||
linen: { cost: 38, profit: 70, duration_ms: 18000 },
|
||||
}
|
||||
|
||||
// MS_PER_SIM_DAY — must match src/constants.js
|
||||
const MS_PER_SIM_DAY = 3_000
|
||||
|
||||
// ── TESSERA world database (read-only) ──────────────────────────────────────
|
||||
@@ -124,8 +108,6 @@ function openPlayerDb(token) {
|
||||
return pdb
|
||||
}
|
||||
|
||||
// ── UUID generator ───────────────────────────────────────────────────────────
|
||||
// Simple RFC4122 v4 UUID — no external dependency needed.
|
||||
function uuid() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = Math.random() * 16 | 0
|
||||
@@ -139,16 +121,14 @@ function migrateJsonToSqlite(token, pdb, json) {
|
||||
const now = new Date().toISOString()
|
||||
const actorId = token
|
||||
const sessionId = token
|
||||
|
||||
const background = json.background_id || 'unknown'
|
||||
const name = json.actor_name || 'Mercator'
|
||||
const epoch = 'roman_14bce'
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT OR IGNORE INTO actor_profile
|
||||
(actor_id, session_id, background_id, actor_name, epoch, schema_version, recorded_at)
|
||||
VALUES (?, ?, ?, ?, ?, 5, ?)
|
||||
`).run(actorId, sessionId, background, name, epoch, now)
|
||||
VALUES (?, ?, ?, ?, 'roman_14bce', 5, ?)
|
||||
`).run(actorId, sessionId, background, name, now)
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT OR IGNORE INTO actor_parameters
|
||||
@@ -173,8 +153,7 @@ function migrateJsonToSqlite(token, pdb, json) {
|
||||
(actor_id, parameter_token, scope, layer,
|
||||
value_true, value_perceived, confidence_tag, observable_level,
|
||||
drift_source, recorded_at)
|
||||
VALUES (?, 'liquiditas', 'actor', 'roman',
|
||||
?, ?, 'measured', 'full', 'migration', ?)
|
||||
VALUES (?, 'liquiditas', 'actor', 'roman', ?, ?, 'measured', 'full', 'migration', ?)
|
||||
`).run(actorId, String(json.den), String(json.den), now)
|
||||
}
|
||||
|
||||
@@ -185,8 +164,7 @@ function migrateJsonToSqlite(token, pdb, json) {
|
||||
(actor_id, parameter_token, scope, layer,
|
||||
value_true, value_perceived, value_social,
|
||||
confidence_tag, observable_level, drift_source, recorded_at)
|
||||
VALUES (?, 'auctoritas', 'actor', 'roman',
|
||||
?, ?, ?, 'indicated', 'partial', 'migration', ?)
|
||||
VALUES (?, 'auctoritas', 'actor', 'roman', ?, ?, ?, 'indicated', 'partial', 'migration', ?)
|
||||
`).run(actorId, band, band, band, now)
|
||||
}
|
||||
|
||||
@@ -196,15 +174,13 @@ function migrateJsonToSqlite(token, pdb, json) {
|
||||
`)
|
||||
for (const ev of (json.events || [])) {
|
||||
insertEvent.run(
|
||||
actorId,
|
||||
ev.type || 'unknown',
|
||||
actorId, ev.type || 'unknown',
|
||||
ev.route_id || null,
|
||||
ev.route_id ? 'venture' : null,
|
||||
JSON.stringify(ev),
|
||||
ev.timestamp_utc || now
|
||||
)
|
||||
}
|
||||
|
||||
insertEvent.run(
|
||||
actorId, 'session_start', null, null,
|
||||
JSON.stringify({ source: 'json_migration', schema_version: 5 }),
|
||||
@@ -232,7 +208,7 @@ function readPlayerState(token, pdb) {
|
||||
|
||||
const paramMap = {}
|
||||
for (const p of params) {
|
||||
paramMap[p.parameter_token] = p
|
||||
if (!paramMap[p.parameter_token]) paramMap[p.parameter_token] = p
|
||||
}
|
||||
|
||||
const den = parseInt(paramMap['liquiditas']?.value_true || '0', 10)
|
||||
@@ -271,7 +247,7 @@ function readPlayerState(token, pdb) {
|
||||
started_utc: ev.timestamp_utc,
|
||||
duration_ms: p.duration_ms || 0,
|
||||
}
|
||||
} catch { /* ignore malformed payload */ }
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
if (ev.type === 'venture_complete' || ev.type === 'dispatch_complete') {
|
||||
active_dispatch = null
|
||||
@@ -317,115 +293,11 @@ function autToInt(band) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Venture helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
// Find the open (active) venture for this actor, if any.
|
||||
function findActiveVenture(pdb, actorId) {
|
||||
return pdb.prepare(`
|
||||
SELECT venture_id, venture_label FROM ventures
|
||||
WHERE actor_id = ? AND status = 'active'
|
||||
ORDER BY recorded_at DESC LIMIT 1
|
||||
`).get(actorId)
|
||||
}
|
||||
|
||||
// Create a venture + venture_leg row on dispatch_start.
|
||||
// Returns the new venture_id.
|
||||
function createVenture(pdb, actorId, routeId, now) {
|
||||
const eco = ROUTE_ECONOMICS[routeId]
|
||||
const cargo = ROUTE_CARGO[routeId]
|
||||
const h3 = ROUTE_H3[routeId]
|
||||
const mode = ROUTE_MODE[routeId] || 'road'
|
||||
|
||||
if (!eco || !cargo || !h3) return null // unknown route — skip silently
|
||||
|
||||
const ventureId = uuid()
|
||||
const legId = uuid()
|
||||
|
||||
const costVectura = Math.round(eco.cost * COST_VECTURA_RATIO * 100) / 100
|
||||
const costPortoria = Math.round(eco.cost * COST_PORTORIA_RATIO * 100) / 100
|
||||
const costOther = Math.round((eco.cost - costVectura - costPortoria) * 100) / 100
|
||||
const durationDays = Math.round(eco.duration_ms / MS_PER_SIM_DAY) // INTEGER
|
||||
|
||||
const label = `${routeId.charAt(0).toUpperCase() + routeId.slice(1)} route`
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT INTO ventures
|
||||
(venture_id, actor_id, venture_label, status,
|
||||
cargo_type, cargo_unit, cost_total,
|
||||
recorded_at, started_at)
|
||||
VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?)
|
||||
`).run(ventureId, actorId, label, cargo.type, cargo.unit,
|
||||
eco.cost, now, now)
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT INTO venture_legs
|
||||
(leg_id, venture_id, leg_sequence,
|
||||
origin_h3, destination_h3, mode,
|
||||
duration_days, cost_vectura, cost_portoria, cost_other, cost_total,
|
||||
status, recorded_at, started_at)
|
||||
VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)
|
||||
`).run(legId, ventureId, h3.origin, h3.destination, mode,
|
||||
durationDays, costVectura, costPortoria, costOther, eco.cost,
|
||||
now, now)
|
||||
|
||||
return ventureId
|
||||
}
|
||||
|
||||
// Close the active venture on dispatch_complete.
|
||||
// Writes outcome_net, completed_at, and closes the leg.
|
||||
function closeVenture(pdb, actorId, routeId, ventureId, now) {
|
||||
const eco = ROUTE_ECONOMICS[routeId]
|
||||
if (!eco) return
|
||||
|
||||
const outcomeNet = eco.profit - eco.cost
|
||||
|
||||
pdb.prepare(`
|
||||
UPDATE ventures
|
||||
SET status = 'complete', revenue_total = ?, outcome_net = ?, completed_at = ?
|
||||
WHERE venture_id = ?
|
||||
`).run(eco.profit, outcomeNet, now, ventureId)
|
||||
|
||||
pdb.prepare(`
|
||||
UPDATE venture_legs
|
||||
SET status = 'complete', completed_at = ?
|
||||
WHERE venture_id = ? AND status = 'active'
|
||||
`).run(now, ventureId)
|
||||
}
|
||||
|
||||
// ── Drift log helpers ────────────────────────────────────────────────────────
|
||||
|
||||
function writeDriftLog(pdb, actorId, paramToken, triggerType, triggerRef,
|
||||
valueBefore, valueAfter, deltaNote, now) {
|
||||
pdb.prepare(`
|
||||
INSERT INTO parameter_drift_log
|
||||
(actor_id, parameter_token, trigger_type, trigger_ref,
|
||||
value_before, value_after, delta_note, recorded_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(actorId, paramToken, triggerType, triggerRef,
|
||||
String(valueBefore), String(valueAfter), deltaNote, now)
|
||||
}
|
||||
|
||||
// ── Write player state ───────────────────────────────────────────────────────
|
||||
|
||||
function writePlayerState(token, pdb, body) {
|
||||
const now = new Date().toISOString()
|
||||
const actorId = token
|
||||
|
||||
// Ensure actor_profile exists
|
||||
const profile = pdb.prepare(
|
||||
'SELECT actor_id FROM actor_profile WHERE actor_id = ? LIMIT 1'
|
||||
).get(actorId)
|
||||
|
||||
if (!profile) {
|
||||
const backgroundId = body.background_id || 'unknown'
|
||||
const actorName = body.actor_name || 'Mercator'
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT OR IGNORE INTO actor_profile
|
||||
(actor_id, session_id, background_id, actor_name, epoch, schema_version, recorded_at)
|
||||
VALUES (?, ?, ?, ?, 'roman_14bce', 5, ?)
|
||||
`).run(actorId, actorId, backgroundId, actorName, now)
|
||||
// ── Seed actor_parameters from background_starting_values ────────────────────
|
||||
// Extracted as a standalone function so it can be called both at new-player
|
||||
// creation and at the background-patch step (Fix 1).
|
||||
|
||||
function seedParameters(pdb, actorId, backgroundId, denOverride, now) {
|
||||
pdb.prepare(`
|
||||
INSERT OR IGNORE INTO actor_parameters
|
||||
(actor_id, parameter_token, scope, layer,
|
||||
@@ -444,7 +316,9 @@ function writePlayerState(token, pdb, body) {
|
||||
WHERE background_id = ?
|
||||
`).run(actorId, now, backgroundId)
|
||||
|
||||
if (body.den !== undefined) {
|
||||
// Override liquiditas with the confirmed starting den from the frontend.
|
||||
// Supersede the seed row immediately so the true starting value is authoritative.
|
||||
if (denOverride !== undefined) {
|
||||
pdb.prepare(`
|
||||
UPDATE actor_parameters
|
||||
SET superseded_at = ?
|
||||
@@ -455,13 +329,132 @@ function writePlayerState(token, pdb, body) {
|
||||
(actor_id, parameter_token, scope, layer,
|
||||
value_true, value_perceived,
|
||||
confidence_tag, observable_level, drift_source, recorded_at)
|
||||
VALUES (?, 'liquiditas', 'actor', 'roman',
|
||||
?, ?, 'measured', 'full', 'initial', ?)
|
||||
`).run(actorId, String(body.den), String(body.den), now)
|
||||
VALUES (?, 'liquiditas', 'actor', 'roman', ?, ?, 'measured', 'full', 'initial', ?)
|
||||
`).run(actorId, String(denOverride), String(denOverride), now)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Venture helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
function findActiveVenture(pdb, actorId) {
|
||||
return pdb.prepare(`
|
||||
SELECT venture_id, venture_label FROM ventures
|
||||
WHERE actor_id = ? AND status = 'active'
|
||||
ORDER BY recorded_at DESC LIMIT 1
|
||||
`).get(actorId)
|
||||
}
|
||||
|
||||
function createVenture(pdb, actorId, routeId, now) {
|
||||
const eco = ROUTE_ECONOMICS[routeId]
|
||||
const cargo = ROUTE_CARGO[routeId]
|
||||
const h3 = ROUTE_H3[routeId]
|
||||
const mode = ROUTE_MODE[routeId] || 'road'
|
||||
if (!eco || !cargo || !h3) return null
|
||||
|
||||
const ventureId = uuid()
|
||||
const legId = uuid()
|
||||
|
||||
const costVectura = Math.round(eco.cost * COST_VECTURA_RATIO * 100) / 100
|
||||
const costPortoria = Math.round(eco.cost * COST_PORTORIA_RATIO * 100) / 100
|
||||
const costOther = Math.round((eco.cost - costVectura - costPortoria) * 100) / 100
|
||||
const durationDays = Math.round(eco.duration_ms / MS_PER_SIM_DAY)
|
||||
|
||||
const label = `${routeId.charAt(0).toUpperCase() + routeId.slice(1)} route`
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT INTO ventures
|
||||
(venture_id, actor_id, venture_label, status,
|
||||
cargo_type, cargo_unit, cost_total,
|
||||
recorded_at, started_at)
|
||||
VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?)
|
||||
`).run(ventureId, actorId, label, cargo.type, cargo.unit, eco.cost, now, now)
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT INTO venture_legs
|
||||
(leg_id, venture_id, leg_sequence,
|
||||
origin_h3, destination_h3, mode,
|
||||
duration_days, cost_vectura, cost_portoria, cost_other, cost_total,
|
||||
status, recorded_at, started_at)
|
||||
VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)
|
||||
`).run(legId, ventureId, h3.origin, h3.destination, mode,
|
||||
durationDays, costVectura, costPortoria, costOther, eco.cost, now, now)
|
||||
|
||||
return ventureId
|
||||
}
|
||||
|
||||
function closeVenture(pdb, actorId, routeId, ventureId, now) {
|
||||
const eco = ROUTE_ECONOMICS[routeId]
|
||||
if (!eco) return
|
||||
const outcomeNet = eco.profit - eco.cost
|
||||
pdb.prepare(`
|
||||
UPDATE ventures
|
||||
SET status = 'complete', revenue_total = ?, outcome_net = ?, completed_at = ?
|
||||
WHERE venture_id = ?
|
||||
`).run(eco.profit, outcomeNet, now, ventureId)
|
||||
pdb.prepare(`
|
||||
UPDATE venture_legs
|
||||
SET status = 'complete', completed_at = ?
|
||||
WHERE venture_id = ? AND status = 'active'
|
||||
`).run(now, ventureId)
|
||||
}
|
||||
|
||||
// ── Drift log ────────────────────────────────────────────────────────────────
|
||||
|
||||
function writeDriftLog(pdb, actorId, paramToken, triggerType, triggerRef,
|
||||
valueBefore, valueAfter, deltaNote, now) {
|
||||
pdb.prepare(`
|
||||
INSERT INTO parameter_drift_log
|
||||
(actor_id, parameter_token, trigger_type, trigger_ref,
|
||||
value_before, value_after, delta_note, recorded_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(actorId, paramToken, triggerType, triggerRef,
|
||||
String(valueBefore), String(valueAfter), deltaNote, now)
|
||||
}
|
||||
|
||||
// ── Write player state ───────────────────────────────────────────────────────
|
||||
|
||||
function writePlayerState(token, pdb, body) {
|
||||
const now = new Date().toISOString()
|
||||
const actorId = token
|
||||
|
||||
const incomingBackground = body.background_id
|
||||
const isRealBackground = incomingBackground && incomingBackground !== 'unknown'
|
||||
|
||||
// Load existing profile (may or may not exist)
|
||||
const profile = pdb.prepare(
|
||||
'SELECT actor_id, background_id FROM actor_profile WHERE actor_id = ? LIMIT 1'
|
||||
).get(actorId)
|
||||
|
||||
if (!profile) {
|
||||
// Brand new player — create profile regardless of background_id.
|
||||
// If background is still 'unknown', parameters are not seeded yet;
|
||||
// they will be seeded on the next POST that arrives with a real background.
|
||||
const backgroundId = incomingBackground || 'unknown'
|
||||
const actorName = body.actor_name || 'Mercator'
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT OR IGNORE INTO actor_profile
|
||||
(actor_id, session_id, background_id, actor_name, epoch, schema_version, recorded_at)
|
||||
VALUES (?, ?, ?, ?, 'roman_14bce', 5, ?)
|
||||
`).run(actorId, actorId, backgroundId, actorName, now)
|
||||
|
||||
if (isRealBackground) {
|
||||
seedParameters(pdb, actorId, backgroundId, body.den, now)
|
||||
}
|
||||
|
||||
// Read current den/aut before any update — needed for drift log
|
||||
} else if (profile.background_id === 'unknown' && isRealBackground) {
|
||||
// Fix 1: profile exists but background was not set on first write.
|
||||
// Patch the profile and seed parameters now.
|
||||
pdb.prepare(`
|
||||
UPDATE actor_profile SET background_id = ?, schema_version = 5 WHERE actor_id = ?
|
||||
`).run(incomingBackground, actorId)
|
||||
|
||||
seedParameters(pdb, actorId, incomingBackground, body.den, now)
|
||||
|
||||
console.log(`[profile] Patched background_id for ${token}: unknown → ${incomingBackground}`)
|
||||
}
|
||||
|
||||
// Read current parameter values — needed for drift log (Fix 2: null check)
|
||||
const currentLiq = pdb.prepare(`
|
||||
SELECT value_true FROM actor_parameters
|
||||
WHERE actor_id = ? AND parameter_token = 'liquiditas' AND superseded_at IS NULL
|
||||
@@ -473,14 +466,14 @@ function writePlayerState(token, pdb, body) {
|
||||
ORDER BY recorded_at DESC LIMIT 1
|
||||
`).get(actorId)
|
||||
|
||||
const prevDen = currentLiq?.value_true ?? '0'
|
||||
const prevAut = currentAut?.value_true ?? 'low'
|
||||
// Fix 2: only record drift when a prior row exists (not during initial seeding)
|
||||
const prevDen = currentLiq?.value_true ?? null
|
||||
const prevAut = currentAut?.value_true ?? null
|
||||
|
||||
// ── Process new events (Change 1 + Change 2) ─────────────────────────────
|
||||
// ── Process new events ───────────────────────────────────────────────────
|
||||
|
||||
const lastRecorded = pdb.prepare(`
|
||||
SELECT recorded_at FROM events
|
||||
WHERE actor_id = ?
|
||||
SELECT recorded_at FROM events WHERE actor_id = ?
|
||||
ORDER BY recorded_at DESC LIMIT 1
|
||||
`).get(actorId)
|
||||
const lastTs = lastRecorded?.recorded_at || '1970-01-01T00:00:00.000Z'
|
||||
@@ -496,26 +489,22 @@ function writePlayerState(token, pdb, body) {
|
||||
|
||||
const routeId = ev.route_id || null
|
||||
|
||||
// Change 1 — venture creation on dispatch_start
|
||||
if (ev.type === 'dispatch_start' && routeId) {
|
||||
const ventureId = createVenture(pdb, actorId, routeId, ts)
|
||||
insertEvent.run(
|
||||
actorId, ev.type, routeId, 'venture',
|
||||
JSON.stringify({ ...ev, venture_id: ventureId }),
|
||||
ts
|
||||
JSON.stringify({ ...ev, venture_id: ventureId }), ts
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Change 1 — venture closure on dispatch_complete
|
||||
if (ev.type === 'dispatch_complete' && routeId) {
|
||||
const active = findActiveVenture(pdb, actorId)
|
||||
if (active) {
|
||||
closeVenture(pdb, actorId, routeId, active.venture_id, ts)
|
||||
insertEvent.run(
|
||||
actorId, ev.type, routeId, 'venture',
|
||||
JSON.stringify({ ...ev, venture_id: active.venture_id }),
|
||||
ts
|
||||
JSON.stringify({ ...ev, venture_id: active.venture_id }), ts
|
||||
)
|
||||
} else {
|
||||
insertEvent.run(
|
||||
@@ -526,23 +515,19 @@ function writePlayerState(token, pdb, body) {
|
||||
continue
|
||||
}
|
||||
|
||||
// All other events — write as-is
|
||||
insertEvent.run(
|
||||
actorId,
|
||||
ev.type || 'unknown',
|
||||
actorId, ev.type || 'unknown',
|
||||
routeId,
|
||||
routeId ? 'venture' : null,
|
||||
JSON.stringify(ev),
|
||||
ts
|
||||
JSON.stringify(ev), ts
|
||||
)
|
||||
}
|
||||
|
||||
// ── Update parameters + drift log (Change 2) ─────────────────────────────
|
||||
// ── Update parameters + drift log ────────────────────────────────────────
|
||||
|
||||
if (body.den !== undefined) {
|
||||
const newDen = String(body.den)
|
||||
if (newDen !== prevDen) {
|
||||
// Find the venture that just completed, if any — use as trigger_ref
|
||||
if (prevDen !== null && newDen !== prevDen) {
|
||||
const lastVenture = pdb.prepare(`
|
||||
SELECT venture_id, venture_label FROM ventures
|
||||
WHERE actor_id = ? AND status = 'complete'
|
||||
@@ -559,14 +544,16 @@ function writePlayerState(token, pdb, body) {
|
||||
lastVenture?.venture_label || null,
|
||||
now
|
||||
)
|
||||
} else if (prevDen === null) {
|
||||
// No prior row — update silently, no drift log entry
|
||||
updateParam(pdb, actorId, 'liquiditas', newDen, newDen, now)
|
||||
}
|
||||
}
|
||||
|
||||
if (body.aut !== undefined) {
|
||||
const newBand = autBand(body.aut)
|
||||
if (newBand !== prevAut) {
|
||||
if (prevAut !== null && newBand !== prevAut) {
|
||||
updateParamWithSocial(pdb, actorId, 'auctoritas', newBand, newBand, newBand, now)
|
||||
|
||||
writeDriftLog(
|
||||
pdb, actorId, 'auctoritas',
|
||||
'interval_complete',
|
||||
@@ -575,17 +562,17 @@ function writePlayerState(token, pdb, body) {
|
||||
'otium rest',
|
||||
now
|
||||
)
|
||||
} else if (prevAut === null) {
|
||||
updateParamWithSocial(pdb, actorId, 'auctoritas', newBand, newBand, newBand, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateParam(pdb, actorId, token, valueTrue, valuePerceived, now) {
|
||||
pdb.prepare(`
|
||||
UPDATE actor_parameters
|
||||
SET superseded_at = ?
|
||||
UPDATE actor_parameters SET superseded_at = ?
|
||||
WHERE actor_id = ? AND parameter_token = ? AND superseded_at IS NULL
|
||||
`).run(now, actorId, token)
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT INTO actor_parameters
|
||||
(actor_id, parameter_token, scope, layer,
|
||||
@@ -597,11 +584,9 @@ function updateParam(pdb, actorId, token, valueTrue, valuePerceived, now) {
|
||||
|
||||
function updateParamWithSocial(pdb, actorId, token, valueTrue, valuePerceived, valueSocial, now) {
|
||||
pdb.prepare(`
|
||||
UPDATE actor_parameters
|
||||
SET superseded_at = ?
|
||||
UPDATE actor_parameters SET superseded_at = ?
|
||||
WHERE actor_id = ? AND parameter_token = ? AND superseded_at IS NULL
|
||||
`).run(now, actorId, token)
|
||||
|
||||
pdb.prepare(`
|
||||
INSERT INTO actor_parameters
|
||||
(actor_id, parameter_token, scope, layer,
|
||||
@@ -615,61 +600,45 @@ function updateParamWithSocial(pdb, actorId, token, valueTrue, valuePerceived, v
|
||||
|
||||
const fastify = Fastify({ logger: false })
|
||||
|
||||
await fastify.register(fastifyStatic, {
|
||||
root: DIST,
|
||||
prefix: '/',
|
||||
})
|
||||
await fastify.register(fastifyStatic, { root: DIST, prefix: '/' })
|
||||
|
||||
fastify.get('/api/map/:h5/:epoch', async (req, reply) => {
|
||||
const { h5: h5param, epoch: epochKey } = req.params
|
||||
|
||||
if (!/^[0-9a-f]{15}$/.test(h5param)) {
|
||||
if (!/^[0-9a-f]{15}$/.test(h5param))
|
||||
return reply.code(400).send({ error: 'Invalid H5 ID' })
|
||||
}
|
||||
if (!/^[a-z0-9_]+$/.test(epochKey)) {
|
||||
if (!/^[a-z0-9_]+$/.test(epochKey))
|
||||
return reply.code(400).send({ error: 'Invalid epoch key' })
|
||||
}
|
||||
|
||||
const epoch = stmtEpoch.get(epochKey)
|
||||
if (!epoch) {
|
||||
return reply.code(404).send({ error: `Unknown epoch: ${epochKey}` })
|
||||
}
|
||||
if (!epoch) return reply.code(404).send({ error: `Unknown epoch: ${epochKey}` })
|
||||
|
||||
let h5int
|
||||
try {
|
||||
h5int = h3HexToInt(h5param)
|
||||
} catch {
|
||||
return reply.code(400).send({ error: 'Malformed H5 ID' })
|
||||
}
|
||||
try { h5int = h3HexToInt(h5param) }
|
||||
catch { return reply.code(400).send({ error: 'Malformed H5 ID' }) }
|
||||
|
||||
const rows = stmtH7.all(epoch.sl_offset_cm, h5int)
|
||||
if (!rows.length) return reply.code(404).send({ error: `No data for H5: ${h5param}` })
|
||||
|
||||
if (!rows.length) {
|
||||
return reply.code(404).send({ error: `No data for H5: ${h5param}` })
|
||||
}
|
||||
|
||||
const cells = rows.map(row => ({
|
||||
return reply.send({
|
||||
epoch_key: epochKey,
|
||||
sl_offset_cm: epoch.sl_offset_cm,
|
||||
h5: h5param,
|
||||
cells: rows.map(row => ({
|
||||
h7: row.h7.toString(16),
|
||||
lat: row.lat,
|
||||
lon: row.lon,
|
||||
h7_land: row.h7_land,
|
||||
h9_total: row.h9_total,
|
||||
is_land: row.h7_land * 2 > row.h9_total ? 1 : 0,
|
||||
}))
|
||||
|
||||
return reply.send({
|
||||
epoch_key: epochKey,
|
||||
sl_offset_cm: epoch.sl_offset_cm,
|
||||
h5: h5param,
|
||||
cells,
|
||||
})),
|
||||
})
|
||||
})
|
||||
|
||||
fastify.get('/api/save/:token', async (req, reply) => {
|
||||
const { token } = req.params
|
||||
if (!/^[0-9a-f]{8}$/.test(token)) {
|
||||
if (!/^[0-9a-f]{8}$/.test(token))
|
||||
return reply.code(400).send({ error: 'Invalid token' })
|
||||
}
|
||||
|
||||
const sqlitePath = join(SAVES_DIR, `${token}.sqlite3`)
|
||||
const jsonPath = join(SAVES_DIR, `${token}.json`)
|
||||
@@ -707,12 +676,10 @@ fastify.get('/api/save/:token', async (req, reply) => {
|
||||
|
||||
fastify.post('/api/save/:token', async (req, reply) => {
|
||||
const { token } = req.params
|
||||
if (!/^[0-9a-f]{8}$/.test(token)) {
|
||||
if (!/^[0-9a-f]{8}$/.test(token))
|
||||
return reply.code(400).send({ error: 'Invalid token' })
|
||||
}
|
||||
if (!req.body || typeof req.body !== 'object') {
|
||||
if (!req.body || typeof req.body !== 'object')
|
||||
return reply.code(400).send({ error: 'Invalid body' })
|
||||
}
|
||||
|
||||
try {
|
||||
const pdb = openPlayerDb(token)
|
||||
@@ -725,13 +692,11 @@ fastify.post('/api/save/:token', async (req, reply) => {
|
||||
}
|
||||
})
|
||||
|
||||
fastify.setNotFoundHandler((req, reply) => {
|
||||
reply.sendFile('index.html')
|
||||
})
|
||||
fastify.setNotFoundHandler((req, reply) => { reply.sendFile('index.html') })
|
||||
|
||||
try {
|
||||
await fastify.listen({ port: 3000, host: '0.0.0.0' })
|
||||
console.log('OTIVM server running on port 3000 — OTIVM-IV (Changes 1+2)')
|
||||
console.log('OTIVM server running on port 3000 — OTIVM-IV (fix: background_id patch)')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user