Observer embryo, Maestro done
This commit is contained in:
+182
-12
@@ -1,6 +1,8 @@
|
||||
import { AccesRights } from '../accesRights.js'
|
||||
import { AgentStore } from './agentStore.js'
|
||||
import { CollisionRegistry } from './collisionRegistry.js'
|
||||
import { ArenaAgentLoader } from './arenaAgentLoader.js'
|
||||
import { SimState, validateAgentSets } from './simulationState.js'
|
||||
import {
|
||||
compareWorldlinePair,
|
||||
positionAt,
|
||||
@@ -19,8 +21,14 @@ export class gpsServer {
|
||||
this.agents = new Map()
|
||||
this.registry = new CollisionRegistry()
|
||||
this.arenaCnx = null
|
||||
this.arenaCnxs = []
|
||||
this.systemCnx = null
|
||||
this.agentStore = null
|
||||
this.arenaLoader = null
|
||||
this.state = SimState.IDLE
|
||||
this.simulationId = null
|
||||
this.bigBangEpoch = null
|
||||
this.ignoredChangeCount = 0
|
||||
}
|
||||
|
||||
getGpsSettings() {
|
||||
@@ -33,8 +41,40 @@ export class gpsServer {
|
||||
})
|
||||
}
|
||||
|
||||
getLifecycleSettings() {
|
||||
const gps = this.gpsConfig.gps ?? {}
|
||||
return({
|
||||
arenaChannel: gps.lifecycle?.arenaChannel ?? 'arena:lifecycle',
|
||||
godsReadyChannel: gps.lifecycle?.godsReadyChannel ?? 'arena:gods:ready',
|
||||
senderId: gps.senderId ?? 'gps',
|
||||
})
|
||||
}
|
||||
|
||||
getArenaStorageSettings() {
|
||||
const gps = this.gpsConfig.gps ?? {}
|
||||
return({
|
||||
agentHashKey: gps.arenaStorage?.agentHashKey ?? 'arena:agents:[UID]',
|
||||
agentsIndexKey: gps.arenaStorage?.agentsIndexKey ?? 'arena:agents',
|
||||
})
|
||||
}
|
||||
|
||||
isLive() {
|
||||
return(this.state === SimState.LIVE)
|
||||
}
|
||||
|
||||
isPrepare() {
|
||||
return(this.state === SimState.PREPARE)
|
||||
}
|
||||
|
||||
simNow() {
|
||||
if(this.bigBangEpoch === null) return(null)
|
||||
return((performance.now() - this.bigBangEpoch) / 1000)
|
||||
}
|
||||
|
||||
now() {
|
||||
return(Date.now() / 1000)
|
||||
if(this.isLive()) return(this.simNow())
|
||||
if(this.isPrepare()) return(0)
|
||||
return(null)
|
||||
}
|
||||
|
||||
wireSystemConnexion(cnx) {
|
||||
@@ -57,7 +97,11 @@ export class gpsServer {
|
||||
|
||||
wireArenaConnexion(cnx) {
|
||||
cnx.gpsSrv = this
|
||||
this.arenaCnx = cnx
|
||||
this.arenaCnxs.push(cnx)
|
||||
if(!this.arenaCnx || cnx.redisConfig.role === 'primary') {
|
||||
this.arenaCnx = cnx
|
||||
this.arenaLoader = new ArenaAgentLoader(cnx, this.getArenaStorageSettings(), this.debug)
|
||||
}
|
||||
}
|
||||
|
||||
getAgent(agentId) {
|
||||
@@ -75,14 +119,16 @@ export class gpsServer {
|
||||
vector: { ...agent.vector },
|
||||
since: agent.since,
|
||||
generation: agent.generation ?? 0,
|
||||
at: new Date(at * 1000).toISOString(),
|
||||
t: at,
|
||||
})
|
||||
}
|
||||
|
||||
getAgentPosition(agentId, at = null) {
|
||||
if(!this.isLive()) return(null)
|
||||
const agent = this.agents.get(agentId)
|
||||
if(!agent) return(null)
|
||||
const t = at ?? this.now()
|
||||
if(t === null) return(null)
|
||||
return(this.buildAgentSnapshot(agent, t))
|
||||
}
|
||||
|
||||
@@ -108,7 +154,9 @@ export class gpsServer {
|
||||
}
|
||||
|
||||
getAgentsInPrism(prism, at = null) {
|
||||
if(!this.isLive()) return([])
|
||||
const t = at ?? this.now()
|
||||
if(t === null) return([])
|
||||
const agents = []
|
||||
for(const agent of this.agents.values()) {
|
||||
const position = positionAt(agent, t)
|
||||
@@ -119,6 +167,117 @@ export class gpsServer {
|
||||
return(agents)
|
||||
}
|
||||
|
||||
async resetSimulation() {
|
||||
this.agents.clear()
|
||||
this.registry = new CollisionRegistry()
|
||||
this.simulationId = null
|
||||
this.bigBangEpoch = null
|
||||
this.ignoredChangeCount = 0
|
||||
this.state = SimState.IDLE
|
||||
await this.agentStore?.clearAll()
|
||||
}
|
||||
|
||||
runInitialPairScan() {
|
||||
const { nearMissDistance, prismTimeHeight } = this.getGpsSettings()
|
||||
const ids = [...this.agents.keys()]
|
||||
for(let i = 0; i < ids.length; i++) {
|
||||
for(let j = i + 1; j < ids.length; j++) {
|
||||
const hit = compareWorldlinePair(
|
||||
this.agents.get(ids[i]),
|
||||
this.agents.get(ids[j]),
|
||||
prismTimeHeight,
|
||||
nearMissDistance,
|
||||
0
|
||||
)
|
||||
if(hit) this.registry.add(hit)
|
||||
}
|
||||
}
|
||||
if(this.debug) console.log(`[GPS] Initial pair scan: ${this.registry.entries.length} proximity entries at T=0`)
|
||||
}
|
||||
|
||||
async publishReadyToStart(result) {
|
||||
if(!this.arenaCnx) return
|
||||
const { godsReadyChannel, senderId } = this.getLifecycleSettings()
|
||||
await this.arenaCnx.redisPublish(godsReadyChannel, {
|
||||
eventType: 'readyToStart',
|
||||
sender: senderId,
|
||||
payload: {
|
||||
success: result.success,
|
||||
simulationId: this.simulationId,
|
||||
agentIds: [...this.agents.keys()],
|
||||
err: result.err ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async onYourMarks(payload = {}) {
|
||||
await this.resetSimulation()
|
||||
this.simulationId = payload.simulationId ?? null
|
||||
if(!this.simulationId) {
|
||||
console.error('[GPS] onYourMarks rejected: missing simulationId')
|
||||
await this.publishReadyToStart({ success: false, err: 'Missing simulationId' })
|
||||
return
|
||||
}
|
||||
if(!this.arenaLoader) {
|
||||
console.error('[GPS] onYourMarks rejected: no arena loader')
|
||||
await this.publishReadyToStart({ success: false, err: 'No arena Redis connection' })
|
||||
return
|
||||
}
|
||||
|
||||
const loadResult = await this.arenaLoader.loadAgents(payload.agentIds ?? null)
|
||||
if(!loadResult.ok) {
|
||||
console.error(`[GPS] onYourMarks load failed: ${loadResult.err}`)
|
||||
await this.publishReadyToStart({ success: false, err: loadResult.err })
|
||||
return
|
||||
}
|
||||
|
||||
for(const agent of loadResult.agents) {
|
||||
this.agents.set(agent.id, agent)
|
||||
}
|
||||
|
||||
if(Array.isArray(payload.agentIds) && payload.agentIds.length) {
|
||||
const mismatch = validateAgentSets(payload.agentIds, [...this.agents.keys()])
|
||||
if(mismatch) {
|
||||
console.error(`[GPS] onYourMarks agent mismatch: ${mismatch}`)
|
||||
await this.resetSimulation()
|
||||
await this.publishReadyToStart({ success: false, err: mismatch })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.runInitialPairScan()
|
||||
this.state = SimState.PREPARE
|
||||
if(this.debug) console.log(`[GPS] PREPARE: ${this.agents.size} agent(s), simulationId=${this.simulationId}`)
|
||||
await this.publishReadyToStart({ success: true })
|
||||
}
|
||||
|
||||
async onBigBang(payload = {}) {
|
||||
if(this.state !== SimState.PREPARE) {
|
||||
console.error(`[GPS] bigBang rejected: expected PREPARE, got ${this.state}`)
|
||||
return
|
||||
}
|
||||
if(!payload.simulationId || payload.simulationId !== this.simulationId) {
|
||||
console.error('[GPS] bigBang rejected: simulationId mismatch')
|
||||
return
|
||||
}
|
||||
if(Array.isArray(payload.agentIds) && payload.agentIds.length) {
|
||||
const mismatch = validateAgentSets(payload.agentIds, [...this.agents.keys()])
|
||||
if(mismatch) {
|
||||
console.error(`[GPS] bigBang agent mismatch: ${mismatch}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.bigBangEpoch = performance.now()
|
||||
this.state = SimState.LIVE
|
||||
|
||||
for(const agent of this.agents.values()) {
|
||||
await this.agentStore?.exportSegment(agent, 'start', 0, this.simulationId)
|
||||
}
|
||||
|
||||
if(this.debug) console.log(`[GPS] LIVE: bigBangEpoch set, simulationId=${this.simulationId}`)
|
||||
}
|
||||
|
||||
upsertAgent(agentId, newVector, newPosition = null) {
|
||||
const now = this.now()
|
||||
let agent = this.agents.get(agentId)
|
||||
@@ -131,7 +290,7 @@ export class gpsServer {
|
||||
generation: 1,
|
||||
}
|
||||
this.agents.set(agentId, agent)
|
||||
this.agentStore?.exportSegment(agent, 'change')
|
||||
this.agentStore?.exportSegment(agent, 'change', now, this.simulationId)
|
||||
return(agent)
|
||||
}
|
||||
|
||||
@@ -140,7 +299,7 @@ export class gpsServer {
|
||||
agent.since = now
|
||||
agent.generation = (agent.generation ?? 0) + 1
|
||||
if(newPosition) agent.position = { ...newPosition }
|
||||
this.agentStore?.exportSegment(agent, 'change')
|
||||
this.agentStore?.exportSegment(agent, 'change', now, this.simulationId)
|
||||
return(agent)
|
||||
}
|
||||
|
||||
@@ -163,7 +322,7 @@ export class gpsServer {
|
||||
|
||||
const hits = this.scanAgentPairs(agentId, refreshed)
|
||||
for(const hit of hits) this.registry.add(hit)
|
||||
this.agentStore?.exportSegment(agent, 'refresh')
|
||||
this.agentStore?.exportSegment(agent, 'refresh', now, this.simulationId)
|
||||
if(this.debug) console.log(`[GPS] Prism refresh: ${agentId}`)
|
||||
return(true)
|
||||
}
|
||||
@@ -196,6 +355,10 @@ export class gpsServer {
|
||||
}
|
||||
|
||||
onVectorChange(agentId, newVector, newPosition = null) {
|
||||
if(!this.isLive()) {
|
||||
if(this.isPrepare()) this.ignoredChangeCount++
|
||||
return([])
|
||||
}
|
||||
this.registry.purge(agentId)
|
||||
this.upsertAgent(agentId, newVector, newPosition)
|
||||
const hits = this.scanAgentPairs(agentId)
|
||||
@@ -205,9 +368,10 @@ export class gpsServer {
|
||||
}
|
||||
|
||||
onAgentRemove(agentId) {
|
||||
if(!this.isLive()) return
|
||||
this.agents.delete(agentId)
|
||||
this.registry.purge(agentId)
|
||||
this.agentStore?.exportRemove(agentId)
|
||||
this.agentStore?.exportRemove(agentId, this.now())
|
||||
if(this.debug) console.log(`[GPS] Agent removed: ${agentId}`)
|
||||
}
|
||||
|
||||
@@ -226,9 +390,9 @@ export class gpsServer {
|
||||
byAgent.get(agentId).push({
|
||||
otherAgent,
|
||||
distance: entry.distance,
|
||||
at: new Date(entry.time * 1000).toISOString(),
|
||||
t: entry.time,
|
||||
minDistance: entry.minDistance,
|
||||
minAt: new Date(entry.minTime * 1000).toISOString(),
|
||||
minT: entry.minTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -240,13 +404,18 @@ export class gpsServer {
|
||||
const chan = this.arenaCnx.config.gps.collisionsChannel.replace(/\[UID\]/g, targetAgentId)
|
||||
await this.arenaCnx.redisPublish(chan, {
|
||||
eventType: 'proximity',
|
||||
payload: { pairs },
|
||||
sender: 'gps',
|
||||
payload: {
|
||||
pairs,
|
||||
simulationId: this.simulationId,
|
||||
},
|
||||
sender: this.getLifecycleSettings().senderId,
|
||||
})
|
||||
}
|
||||
|
||||
async tickCollisions() {
|
||||
const due = this.registry.dueBefore(this.now())
|
||||
const now = this.now()
|
||||
if(now === null) return
|
||||
const due = this.registry.dueBefore(now)
|
||||
if(!due.length) return
|
||||
|
||||
const batches = this.buildProximityBatches(due)
|
||||
@@ -257,6 +426,7 @@ export class gpsServer {
|
||||
}
|
||||
|
||||
tickArena() {
|
||||
if(!this.isLive()) return
|
||||
this.tickPrismRefresh()
|
||||
this.tickCollisions()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user