maestro refacto to grrom, getpositions actions transfered to Observer, changed to frustum, with subscription
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { methods as utilities, construct as utilitiesConstruct } from './utilities.js'
|
import { methods as utilities, construct as utilitiesConstruct } from './utilities.js'
|
||||||
import { methods as positions } from './positions.js'
|
|
||||||
import { dispatchMessage } from './dispatch.js'
|
import { dispatchMessage } from './dispatch.js'
|
||||||
|
|
||||||
export const afterLoginMethods = [
|
export const afterLoginMethods = [
|
||||||
@@ -7,6 +6,5 @@ export const afterLoginMethods = [
|
|||||||
]
|
]
|
||||||
export const meshActions = {
|
export const meshActions = {
|
||||||
...utilities,
|
...utilities,
|
||||||
...positions,
|
|
||||||
}
|
}
|
||||||
export { dispatchMessage }
|
export { dispatchMessage }
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
import { publishActionReply, parseSimTime } from '../../actionsHelper.js'
|
|
||||||
|
|
||||||
export const methods = {
|
|
||||||
|
|
||||||
/* Event-Rx:
|
|
||||||
{
|
|
||||||
"action": "GETAGENTPOSITION",
|
|
||||||
"reqid": "6az5e4r6a",
|
|
||||||
"payload": {
|
|
||||||
"agentId": "agent42",
|
|
||||||
"t": 12.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event-Tx:
|
|
||||||
{
|
|
||||||
"action": "GETAGENTPOSITION",
|
|
||||||
"success": true,
|
|
||||||
"reqid": "6az5e4r6a",
|
|
||||||
"payload": {
|
|
||||||
"agent": {
|
|
||||||
"id": "agent42",
|
|
||||||
"position": { "x": 1, "y": 2, "z": 3 },
|
|
||||||
"vector": { "x": 0, "y": 0, "z": 0 },
|
|
||||||
"since": 0,
|
|
||||||
"generation": 2,
|
|
||||||
"t": 12.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
async action_GETAGENTPOSITION(action, payload, reqid, sender, roles) {
|
|
||||||
const replyOpts = {
|
|
||||||
action,
|
|
||||||
reqid,
|
|
||||||
sender,
|
|
||||||
replyChannel: this.config.gps.gpsActionsReply,
|
|
||||||
}
|
|
||||||
if(!this.accessRights.canDo(roles, action)) {
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: false,
|
|
||||||
err: 'Unauthorized action !',
|
|
||||||
} })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!this.gpsSrv.isLive()) {
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: false,
|
|
||||||
err: 'Simulation not live',
|
|
||||||
} })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentId = payload?.agentId
|
|
||||||
if(!agentId || typeof(agentId) !== 'string') {
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: false,
|
|
||||||
err: 'Missing or invalid agentId',
|
|
||||||
} })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const at = parseSimTime(payload, () => this.gpsSrv.now())
|
|
||||||
if(at === null) {
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: false,
|
|
||||||
err: 'Invalid simulation time',
|
|
||||||
} })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const agent = this.gpsSrv.getAgentPosition(agentId, at)
|
|
||||||
if(!agent) {
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: false,
|
|
||||||
err: `Unknown agent: ${agentId}`,
|
|
||||||
} })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: true,
|
|
||||||
payload: { agent },
|
|
||||||
} })
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Event-Rx:
|
|
||||||
{
|
|
||||||
"action": "GETAGENTSINPRISM",
|
|
||||||
"reqid": "6az5e4r6a",
|
|
||||||
"payload": {
|
|
||||||
"prism": {
|
|
||||||
"xMin": -10, "xMax": 10,
|
|
||||||
"yMin": -10, "yMax": 10,
|
|
||||||
"zMin": 0, "zMax": 5
|
|
||||||
},
|
|
||||||
"t": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
async action_GETAGENTSINPRISM(action, payload, reqid, sender, roles) {
|
|
||||||
const replyOpts = {
|
|
||||||
action,
|
|
||||||
reqid,
|
|
||||||
sender,
|
|
||||||
replyChannel: this.config.gps.gpsActionsReply,
|
|
||||||
}
|
|
||||||
if(!this.accessRights.canDo(roles, action)) {
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: false,
|
|
||||||
err: 'Unauthorized action !',
|
|
||||||
} })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!this.gpsSrv.isLive()) {
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: false,
|
|
||||||
err: 'Simulation not live',
|
|
||||||
} })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const prism = payload?.prism
|
|
||||||
if(!this.gpsSrv.isValidPrism(prism)) {
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: false,
|
|
||||||
err: 'Missing or invalid prism bounds',
|
|
||||||
} })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const at = parseSimTime(payload, () => this.gpsSrv.now())
|
|
||||||
if(at === null) {
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: false,
|
|
||||||
err: 'Invalid simulation time',
|
|
||||||
} })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const agents = this.gpsSrv.getAgentsInPrism(prism, at)
|
|
||||||
publishActionReply(this, { ...replyOpts, reply: {
|
|
||||||
success: true,
|
|
||||||
payload: {
|
|
||||||
agents,
|
|
||||||
t: at,
|
|
||||||
},
|
|
||||||
} })
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
+11
-11
@@ -1,23 +1,23 @@
|
|||||||
|
|
||||||
export class AgentStore {
|
export class AgentStore {
|
||||||
|
|
||||||
constructor(systemCnx, storage, debug = false) {
|
constructor(systemCnx, gpsStorage, debug = false) {
|
||||||
this.cnx = systemCnx
|
this.cnx = systemCnx
|
||||||
this.storage = storage
|
this.gpsStorage = gpsStorage
|
||||||
this.debug = debug
|
this.debug = debug
|
||||||
}
|
}
|
||||||
|
|
||||||
agentHashKey(agentId) {
|
agentHashKey(agentId) {
|
||||||
return(this.storage.agentHashKey.replace(/\[UID\]/g, agentId))
|
return(this.gpsStorage.agentHashKey.replace(/\[UID\]/g, agentId))
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearAll() {
|
async clearAll() {
|
||||||
try {
|
try {
|
||||||
const ids = await this.cnx.redisSmembers(this.storage.agentsIndexKey)
|
const ids = await this.cnx.redisSmembers(this.gpsStorage.agentsIndexKey)
|
||||||
for(const id of ids) {
|
for(const id of ids) {
|
||||||
await this.cnx.redisDel(this.agentHashKey(id))
|
await this.cnx.redisDel(this.agentHashKey(id))
|
||||||
}
|
}
|
||||||
await this.cnx.redisDel(this.storage.agentsIndexKey)
|
await this.cnx.redisDel(this.gpsStorage.agentsIndexKey)
|
||||||
if(this.debug) console.log(`[GPS] Cleared system agent store (${ids.length} agent(s))`)
|
if(this.debug) console.log(`[GPS] Cleared system agent store (${ids.length} agent(s))`)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error('[GPS] Failed to clear system agent store:', err)
|
console.error('[GPS] Failed to clear system agent store:', err)
|
||||||
@@ -37,11 +37,11 @@ export class AgentStore {
|
|||||||
simulationId,
|
simulationId,
|
||||||
}
|
}
|
||||||
await this.cnx.redisHset(this.agentHashKey(agent.id), 'segment', record)
|
await this.cnx.redisHset(this.agentHashKey(agent.id), 'segment', record)
|
||||||
await this.cnx.redisSadd(this.storage.agentsIndexKey, agent.id)
|
await this.cnx.redisSadd(this.gpsStorage.agentsIndexKey, agent.id)
|
||||||
await this.cnx.redisXadd(
|
await this.cnx.redisXadd(
|
||||||
this.storage.positionsStream,
|
this.gpsStorage.positionsStream,
|
||||||
record,
|
record,
|
||||||
this.storage.streamMaxLen ?? ''
|
this.gpsStorage.streamMaxLen ?? ''
|
||||||
)
|
)
|
||||||
if(this.debug) console.log(`[GPS] Exported segment ${agent.id} (${eventType})`)
|
if(this.debug) console.log(`[GPS] Exported segment ${agent.id} (${eventType})`)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
@@ -57,11 +57,11 @@ export class AgentStore {
|
|||||||
t: simT,
|
t: simT,
|
||||||
}
|
}
|
||||||
await this.cnx.redisDel(this.agentHashKey(agentId))
|
await this.cnx.redisDel(this.agentHashKey(agentId))
|
||||||
await this.cnx.redisSrem(this.storage.agentsIndexKey, agentId)
|
await this.cnx.redisSrem(this.gpsStorage.agentsIndexKey, agentId)
|
||||||
await this.cnx.redisXadd(
|
await this.cnx.redisXadd(
|
||||||
this.storage.positionsStream,
|
this.gpsStorage.positionsStream,
|
||||||
record,
|
record,
|
||||||
this.storage.streamMaxLen ?? ''
|
this.gpsStorage.streamMaxLen ?? ''
|
||||||
)
|
)
|
||||||
if(this.debug) console.log(`[GPS] Exported remove ${agentId}`)
|
if(this.debug) console.log(`[GPS] Exported remove ${agentId}`)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
export class ArenaAgentLoader {
|
export class ArenaAgentLoader {
|
||||||
|
|
||||||
constructor(arenaCnx, storage, debug = false) {
|
constructor(arenaCnx, arenaStorage, debug = false) {
|
||||||
this.cnx = arenaCnx
|
this.cnx = arenaCnx
|
||||||
this.storage = storage
|
this.arenaStorage = arenaStorage
|
||||||
this.debug = debug
|
this.debug = debug
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,11 +45,11 @@ export class ArenaAgentLoader {
|
|||||||
|
|
||||||
async #listAgentIds(expectedIds = null) {
|
async #listAgentIds(expectedIds = null) {
|
||||||
if(Array.isArray(expectedIds) && expectedIds.length) return([...expectedIds])
|
if(Array.isArray(expectedIds) && expectedIds.length) return([...expectedIds])
|
||||||
return(await this.cnx.redisSmembers(this.storage.agentsIndexKey))
|
return(await this.cnx.redisSmembers(this.arenaStorage.agentsIndexKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
async #loadAgentFromHash(agentId) {
|
async #loadAgentFromHash(agentId) {
|
||||||
const key = this.storage.agentHashKey.replace(/\[UID\]/g, agentId)
|
const key = this.arenaStorage.agentHashKey.replace(/\[UID\]/g, agentId)
|
||||||
const positionRaw = await this.cnx.redisHget(key, 'position')
|
const positionRaw = await this.cnx.redisHget(key, 'position')
|
||||||
const vectorRaw = await this.cnx.redisHget(key, 'vector')
|
const vectorRaw = await this.cnx.redisHget(key, 'vector')
|
||||||
let position = this.#parseHashField(positionRaw)
|
let position = this.#parseHashField(positionRaw)
|
||||||
|
|||||||
+3
-3
@@ -89,9 +89,9 @@ export class gpsServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initAgentStore() {
|
initAgentStore() {
|
||||||
const storage = this.gpsConfig.gps?.storage
|
const gpsStorage = this.gpsConfig.gps?.GPSstorage
|
||||||
if(storage && this.systemCnx) {
|
if(gpsStorage && this.systemCnx) {
|
||||||
this.agentStore = new AgentStore(this.systemCnx, storage, this.debug)
|
this.agentStore = new AgentStore(this.systemCnx, gpsStorage, this.debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -a
|
||||||
. /etc/p42/secrets.env
|
. /etc/p42/secrets.env
|
||||||
|
set +a
|
||||||
|
|
||||||
daemon=p42Gps
|
daemon=p42Gps
|
||||||
logfile=gps.log
|
logfile=gps.log
|
||||||
|
|||||||
@@ -4,8 +4,28 @@ export const construct = (redisCnx) => {
|
|||||||
|
|
||||||
export const methods = {
|
export const methods = {
|
||||||
|
|
||||||
|
handleLifecycleEvent(msg) {
|
||||||
|
const srv = this.observerSrv
|
||||||
|
if(!srv) return
|
||||||
|
|
||||||
|
if(msg.eventType === 'onYourMarks') {
|
||||||
|
srv.onYourMarks()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msg.eventType === 'bigBang') {
|
||||||
|
srv.onBigBang()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
dispatchArenaMessage(msg, chan) {
|
dispatchArenaMessage(msg, chan) {
|
||||||
if(this.debug) console.log(`[${this.redisId}] Arena message (unhandled):`, msg.eventType, chan)
|
const observer = this.config.observer
|
||||||
|
if(!observer || !this.observerSrv) return(false)
|
||||||
|
|
||||||
|
if(this.matchesChan(chan, observer.lifecycle?.arenaChannel ?? 'arena:lifecycle')) {
|
||||||
|
this.handleLifecycleEvent(msg)
|
||||||
|
return(true)
|
||||||
|
}
|
||||||
return(false)
|
return(false)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { methods as utilities, construct as utilitiesConstruct } from './utilities.js'
|
import { methods as utilities, construct as utilitiesConstruct } from './utilities.js'
|
||||||
|
import { methods as positions } from './positions.js'
|
||||||
import { dispatchMessage } from './dispatch.js'
|
import { dispatchMessage } from './dispatch.js'
|
||||||
|
|
||||||
export const afterLoginMethods = [
|
export const afterLoginMethods = [
|
||||||
@@ -7,6 +8,7 @@ export const afterLoginMethods = [
|
|||||||
|
|
||||||
export const meshActions = {
|
export const meshActions = {
|
||||||
...utilities,
|
...utilities,
|
||||||
|
...positions,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { dispatchMessage }
|
export { dispatchMessage }
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import { publishActionReply, parseSimTime } from '../../actionsHelper.js'
|
||||||
|
import { Frustum } from '../../frustum.js'
|
||||||
|
|
||||||
|
export const methods = {
|
||||||
|
|
||||||
|
/* Event-Rx:
|
||||||
|
{
|
||||||
|
"action": "GETAGENTPOSITION",
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
"payload": {
|
||||||
|
"agentId": "agent42",
|
||||||
|
"t": 12.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event-Tx:
|
||||||
|
{
|
||||||
|
"action": "GETAGENTPOSITION",
|
||||||
|
"success": true,
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
"payload": {
|
||||||
|
"agent": {
|
||||||
|
"id": "agent42",
|
||||||
|
"position": { "x": 1, "y": 2, "z": 3 },
|
||||||
|
"vector": { "x": 0, "y": 0, "z": 0 },
|
||||||
|
"since": 0,
|
||||||
|
"generation": 2,
|
||||||
|
"t": 12.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
async action_GETAGENTPOSITION(action, payload, reqid, sender, roles) {
|
||||||
|
const replyOpts = {
|
||||||
|
action,
|
||||||
|
reqid,
|
||||||
|
sender,
|
||||||
|
replyChannel: this.config.observer.observerActionsReply,
|
||||||
|
}
|
||||||
|
if(!this.accessRights.canDo(roles, action)) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Unauthorized action !',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = this.observerSrv.gpsStorageReader
|
||||||
|
if(!reader) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'GPS storage reader not ready',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.observerSrv.isLive()) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Simulation not live',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentId = payload?.agentId
|
||||||
|
if(!agentId || typeof(agentId) !== 'string') {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Missing or invalid agentId',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const at = parseSimTime(payload, () => this.observerSrv.now())
|
||||||
|
if(at === null) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Invalid simulation time',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = await reader.getAgentPosition(agentId, at)
|
||||||
|
if(!agent) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: `Unknown agent: ${agentId}`,
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: true,
|
||||||
|
payload: { agent },
|
||||||
|
} })
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Event-Rx:
|
||||||
|
{
|
||||||
|
"action": "GETAGENTSINFRUSTUM",
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
"payload": {
|
||||||
|
"planes": [
|
||||||
|
{ "nx": 1, "ny": 0, "nz": 0, "d": -10 },
|
||||||
|
{ "nx": -1, "ny": 0, "nz": 0, "d": 10 },
|
||||||
|
{ "nx": 0, "ny": 1, "nz": 0, "d": -10 },
|
||||||
|
{ "nx": 0, "ny": -1, "nz": 0, "d": 10 },
|
||||||
|
{ "nx": 0, "ny": 0, "nz": 1, "d": 0 },
|
||||||
|
{ "nx": 0, "ny": 0, "nz": -1, "d": 5 }
|
||||||
|
],
|
||||||
|
"t": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
async action_GETAGENTSINFRUSTUM(action, payload, reqid, sender, roles) {
|
||||||
|
const replyOpts = {
|
||||||
|
action,
|
||||||
|
reqid,
|
||||||
|
sender,
|
||||||
|
replyChannel: this.config.observer.observerActionsReply,
|
||||||
|
}
|
||||||
|
if(!this.accessRights.canDo(roles, action)) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Unauthorized action !',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = this.observerSrv.requestorRegistry
|
||||||
|
if(!registry) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Requestor registry not ready',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.observerSrv.isLive()) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Simulation not live',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const frustum = Frustum.fromPlanes(payload?.planes)
|
||||||
|
if(!frustum) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Missing or invalid frustum planes (expected 6)',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const at = parseSimTime(payload, () => this.observerSrv.now())
|
||||||
|
if(at === null) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Invalid simulation time',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await registry.evaluateOnce({ frustum, t: at })
|
||||||
|
if(!result.ok) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: result.err,
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
agents: result.agents,
|
||||||
|
t: result.t,
|
||||||
|
},
|
||||||
|
} })
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Event-Rx:
|
||||||
|
{
|
||||||
|
"action": "SUBSCRIBEFRUSTUM",
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
"sender": "client-uuid",
|
||||||
|
"payload": {
|
||||||
|
"planes": [
|
||||||
|
{ "nx": 1, "ny": 0, "nz": 0, "d": -10 },
|
||||||
|
{ "nx": -1, "ny": 0, "nz": 0, "d": 10 },
|
||||||
|
{ "nx": 0, "ny": 1, "nz": 0, "d": -10 },
|
||||||
|
{ "nx": 0, "ny": -1, "nz": 0, "d": 10 },
|
||||||
|
{ "nx": 0, "ny": 0, "nz": 1, "d": 0 },
|
||||||
|
{ "nx": 0, "ny": 0, "nz": -1, "d": 5 }
|
||||||
|
],
|
||||||
|
"frequency": 800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event-Tx:
|
||||||
|
{
|
||||||
|
"action": "SUBSCRIBEFRUSTUM",
|
||||||
|
"success": true,
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
"payload": {
|
||||||
|
"frequency": 900,
|
||||||
|
"agents": [ ... ],
|
||||||
|
"t": 12.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Periodic push (no reqid):
|
||||||
|
{
|
||||||
|
"action": "GETAGENTSINFRUSTUM",
|
||||||
|
"success": true,
|
||||||
|
"sender": "observer",
|
||||||
|
"payload": { "agents": [ ... ], "t": 12.5 }
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
async action_SUBSCRIBEFRUSTUM(action, payload, reqid, sender, roles) {
|
||||||
|
const replyOpts = {
|
||||||
|
action,
|
||||||
|
reqid,
|
||||||
|
sender,
|
||||||
|
replyChannel: this.config.observer.observerActionsReply,
|
||||||
|
}
|
||||||
|
if(!this.accessRights.canDo(roles, action)) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Unauthorized action !',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!sender || typeof(sender) !== 'string') {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Missing or invalid sender',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = this.observerSrv.requestorRegistry
|
||||||
|
if(!registry) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Requestor registry not ready',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.observerSrv.isLive()) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Simulation not live',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await registry.subscribeFrustum(sender, {
|
||||||
|
planes: payload?.planes,
|
||||||
|
frequency: payload?.frequency,
|
||||||
|
})
|
||||||
|
if(!result.ok) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: result.err,
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
frequency: result.frequency,
|
||||||
|
agents: result.agents,
|
||||||
|
t: result.t,
|
||||||
|
},
|
||||||
|
} })
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
@@ -14,3 +14,9 @@ export function publishActionReply(redisCnx, options) {
|
|||||||
const chan = replyChannel.replace(/\[UID\]/g, sender)
|
const chan = replyChannel.replace(/\[UID\]/g, sender)
|
||||||
redisCnx.redisPublish(chan, reply)
|
redisCnx.redisPublish(chan, reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseSimTime(payload, fallbackFn) {
|
||||||
|
if(payload?.t != null && typeof(payload.t) === 'number' && !Number.isNaN(payload.t)) return(payload.t)
|
||||||
|
if(payload?.at != null && typeof(payload.at) === 'number' && !Number.isNaN(payload.at)) return(payload.at)
|
||||||
|
return(fallbackFn())
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export class Frustum {
|
||||||
|
|
||||||
|
constructor(planes) {
|
||||||
|
if(!Frustum.#validatePlanes(planes)) throw(new Error('Invalid frustum planes'))
|
||||||
|
this.planes = planes.map(p => ({ ...p }))
|
||||||
|
}
|
||||||
|
|
||||||
|
static #validatePlanes(planes) {
|
||||||
|
if(!Array.isArray(planes) || planes.length !== 6) return(false)
|
||||||
|
for(const p of planes) {
|
||||||
|
if(!p || typeof(p) !== 'object') return(false)
|
||||||
|
if(typeof(p.nx) !== 'number' || Number.isNaN(p.nx)) return(false)
|
||||||
|
if(typeof(p.ny) !== 'number' || Number.isNaN(p.ny)) return(false)
|
||||||
|
if(typeof(p.nz) !== 'number' || Number.isNaN(p.nz)) return(false)
|
||||||
|
if(typeof(p.d) !== 'number' || Number.isNaN(p.d)) return(false)
|
||||||
|
}
|
||||||
|
return(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromPlanes(planes) {
|
||||||
|
if(!Frustum.#validatePlanes(planes)) return(null)
|
||||||
|
return(new Frustum(planes))
|
||||||
|
}
|
||||||
|
|
||||||
|
containsPoint(position) {
|
||||||
|
for(const p of this.planes) {
|
||||||
|
const dist = p.nx * position.x + p.ny * position.y + p.nz * position.z + p.d
|
||||||
|
if(dist < 0) return(false)
|
||||||
|
}
|
||||||
|
return(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { positionAt } from '../GPS/actions/arena/worldline.js'
|
||||||
|
|
||||||
|
export class GpsStorageReader {
|
||||||
|
|
||||||
|
constructor(systemCnx, gpsStorage, debug = false) {
|
||||||
|
this.cnx = systemCnx
|
||||||
|
this.gpsStorage = gpsStorage
|
||||||
|
this.debug = debug
|
||||||
|
}
|
||||||
|
|
||||||
|
agentHashKey(agentId) {
|
||||||
|
return(this.gpsStorage.agentHashKey.replace(/\[UID\]/g, agentId))
|
||||||
|
}
|
||||||
|
|
||||||
|
#parseHashField(raw) {
|
||||||
|
if(raw == null) return(null)
|
||||||
|
if(typeof(raw) === 'object') return(raw)
|
||||||
|
try { return(JSON.parse(raw)) }
|
||||||
|
catch { return(null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#segmentToAgent(segment) {
|
||||||
|
if(!segment?.id) return(null)
|
||||||
|
if(!this.#isVector(segment.position) || !this.#isVector(segment.vector)) return(null)
|
||||||
|
return({
|
||||||
|
id: segment.id,
|
||||||
|
position: { ...segment.position },
|
||||||
|
vector: { ...segment.vector },
|
||||||
|
since: segment.since ?? 0,
|
||||||
|
generation: segment.generation ?? 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#isVector(v) {
|
||||||
|
return(
|
||||||
|
v &&
|
||||||
|
typeof(v) === 'object' &&
|
||||||
|
typeof(v.x) === 'number' &&
|
||||||
|
typeof(v.y) === 'number' &&
|
||||||
|
typeof(v.z) === 'number'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAgentSnapshot(agent, at) {
|
||||||
|
return({
|
||||||
|
id: agent.id,
|
||||||
|
position: positionAt(agent, at),
|
||||||
|
vector: { ...agent.vector },
|
||||||
|
since: agent.since,
|
||||||
|
generation: agent.generation ?? 0,
|
||||||
|
t: at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSegment(agentId) {
|
||||||
|
const raw = await this.cnx.redisHget(this.agentHashKey(agentId), 'segment')
|
||||||
|
return(this.#parseHashField(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
async listAgentIds() {
|
||||||
|
return(await this.cnx.redisSmembers(this.gpsStorage.agentsIndexKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAllAgents() {
|
||||||
|
const ids = await this.listAgentIds()
|
||||||
|
const agents = new Map()
|
||||||
|
for(const id of ids) {
|
||||||
|
const segment = await this.loadSegment(id)
|
||||||
|
const agent = this.#segmentToAgent(segment)
|
||||||
|
if(agent) agents.set(id, agent)
|
||||||
|
}
|
||||||
|
return(agents)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAgentPosition(agentId, at) {
|
||||||
|
const segment = await this.loadSegment(agentId)
|
||||||
|
const agent = this.#segmentToAgent(segment)
|
||||||
|
if(!agent) return(null)
|
||||||
|
return(this.buildAgentSnapshot(agent, at))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import { AccesRights } from '../accesRights.js'
|
import { AccesRights } from '../accesRights.js'
|
||||||
|
import { GpsStorageReader } from './gpsStorageReader.js'
|
||||||
|
import { RequestorRegistry } from './requestorRegistry.js'
|
||||||
|
import { publishActionReply } from './actionsHelper.js'
|
||||||
|
import { SimState } from '../GPS/simulationState.js'
|
||||||
|
|
||||||
export class observerServer {
|
export class observerServer {
|
||||||
|
|
||||||
@@ -11,12 +15,17 @@ export class observerServer {
|
|||||||
this.arenaCnx = null
|
this.arenaCnx = null
|
||||||
this.arenaCnxs = []
|
this.arenaCnxs = []
|
||||||
this.systemCnx = null
|
this.systemCnx = null
|
||||||
|
this.gpsStorageReader = null
|
||||||
|
this.requestorRegistry = null
|
||||||
|
this.state = SimState.IDLE
|
||||||
|
this.bigBangEpoch = null
|
||||||
}
|
}
|
||||||
|
|
||||||
getObserverSettings() {
|
getObserverSettings() {
|
||||||
const observer = this.observerConfig.observer ?? {}
|
const observer = this.observerConfig.observer ?? {}
|
||||||
return({
|
return({
|
||||||
senderId: observer.senderId ?? 'observer',
|
senderId: observer.senderId ?? 'observer',
|
||||||
|
scanIntervalMs: observer.scanIntervalMs ?? 300,
|
||||||
lifecycle: {
|
lifecycle: {
|
||||||
arenaChannel: observer.lifecycle?.arenaChannel ?? 'arena:lifecycle',
|
arenaChannel: observer.lifecycle?.arenaChannel ?? 'arena:lifecycle',
|
||||||
godsReadyChannel: observer.lifecycle?.godsReadyChannel ?? 'arena:gods:ready',
|
godsReadyChannel: observer.lifecycle?.godsReadyChannel ?? 'arena:gods:ready',
|
||||||
@@ -24,6 +33,70 @@ export class observerServer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGpsStorageSettings() {
|
||||||
|
const gps = this.observerConfig.gps ?? {}
|
||||||
|
return(gps.GPSstorage ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
initGpsStorageReader() {
|
||||||
|
const gpsStorage = this.getGpsStorageSettings()
|
||||||
|
if(gpsStorage && this.systemCnx) {
|
||||||
|
this.gpsStorageReader = new GpsStorageReader(this.systemCnx, gpsStorage, this.debug)
|
||||||
|
this.initRequestorRegistry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initRequestorRegistry() {
|
||||||
|
if(!this.gpsStorageReader || this.requestorRegistry) return
|
||||||
|
const { scanIntervalMs } = this.getObserverSettings()
|
||||||
|
this.requestorRegistry = new RequestorRegistry(
|
||||||
|
this.gpsStorageReader,
|
||||||
|
() => this.now(),
|
||||||
|
scanIntervalMs,
|
||||||
|
(sender, payload) => this.publishFrustumUpdate(sender, payload),
|
||||||
|
this.debug
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishFrustumUpdate(sender, payload) {
|
||||||
|
if(!this.systemCnx || !sender) return
|
||||||
|
const observer = this.observerConfig.observer ?? {}
|
||||||
|
publishActionReply(this.systemCnx, {
|
||||||
|
action: 'GETAGENTSINFRUSTUM',
|
||||||
|
sender,
|
||||||
|
replyChannel: observer.observerActionsReply ?? 'system:replies:[UID]',
|
||||||
|
reply: {
|
||||||
|
success: true,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isLive() {
|
||||||
|
return(this.state === SimState.LIVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
simNow() {
|
||||||
|
if(this.bigBangEpoch === null) return(null)
|
||||||
|
return((performance.now() - this.bigBangEpoch) / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
now() {
|
||||||
|
if(this.isLive()) return(this.simNow())
|
||||||
|
return(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
onYourMarks() {
|
||||||
|
this.state = SimState.PREPARE
|
||||||
|
this.bigBangEpoch = null
|
||||||
|
this.requestorRegistry?.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
onBigBang() {
|
||||||
|
this.bigBangEpoch = performance.now()
|
||||||
|
this.state = SimState.LIVE
|
||||||
|
}
|
||||||
|
|
||||||
wireSystemConnexion(cnx) {
|
wireSystemConnexion(cnx) {
|
||||||
cnx.observerSrv = this
|
cnx.observerSrv = this
|
||||||
cnx.accessRights = this.accessRights
|
cnx.accessRights = this.accessRights
|
||||||
@@ -31,6 +104,7 @@ export class observerServer {
|
|||||||
cnx.getAccessRights = () => this.getAccessRights()
|
cnx.getAccessRights = () => this.getAccessRights()
|
||||||
if(!this.systemCnx || cnx.redisConfig.role === 'primary') {
|
if(!this.systemCnx || cnx.redisConfig.role === 'primary') {
|
||||||
this.systemCnx = cnx
|
this.systemCnx = cnx
|
||||||
|
this.initGpsStorageReader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { positionAt } from '../GPS/actions/arena/worldline.js'
|
||||||
|
import { Frustum } from './frustum.js'
|
||||||
|
|
||||||
|
export class RequestorRegistry {
|
||||||
|
|
||||||
|
constructor(reader, getNow, scanIntervalMs, onPush, debug = false) {
|
||||||
|
this.reader = reader
|
||||||
|
this.getNow = getNow
|
||||||
|
this.scanIntervalMs = scanIntervalMs
|
||||||
|
this.onPush = onPush
|
||||||
|
this.debug = debug
|
||||||
|
this.requestors = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
#tickTimer = null
|
||||||
|
#scanInFlight = false
|
||||||
|
|
||||||
|
#adjustFrequency(requestedMs) {
|
||||||
|
if(typeof(requestedMs) !== 'number' || Number.isNaN(requestedMs)) return(null)
|
||||||
|
if(requestedMs < 300 || requestedMs > 10000) return(null)
|
||||||
|
const tickMs = this.scanIntervalMs
|
||||||
|
const ticks = Math.round(requestedMs / tickMs)
|
||||||
|
const minTicks = Math.ceil(300 / tickMs)
|
||||||
|
const maxTicks = Math.floor(10000 / tickMs)
|
||||||
|
const clampedTicks = Math.max(minTicks, Math.min(maxTicks, ticks))
|
||||||
|
return(clampedTicks * tickMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribeFrustum(id, { planes, frequency }) {
|
||||||
|
if(!id || typeof(id) !== 'string') return({ ok: false, err: 'Invalid requestor id' })
|
||||||
|
const frustum = Frustum.fromPlanes(planes)
|
||||||
|
if(!frustum) return({ ok: false, err: 'Invalid frustum planes' })
|
||||||
|
|
||||||
|
const frequencyMs = this.#adjustFrequency(frequency)
|
||||||
|
if(frequencyMs === null) {
|
||||||
|
return({ ok: false, err: 'Invalid frequency (expected 300–10000 ms)' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = this.getNow()
|
||||||
|
if(t === null) return({ ok: false, err: 'Simulation not live' })
|
||||||
|
|
||||||
|
const agents = await this.reader.loadAllAgents()
|
||||||
|
const matching = this.#matchAgents(agents, frustum, t)
|
||||||
|
const pushEveryNTicks = frequencyMs / this.scanIntervalMs
|
||||||
|
|
||||||
|
this.requestors.set(id, {
|
||||||
|
id,
|
||||||
|
frustum,
|
||||||
|
tMode: 'live',
|
||||||
|
subscription: true,
|
||||||
|
frequencyMs,
|
||||||
|
pushEveryNTicks,
|
||||||
|
tickCounter: 0,
|
||||||
|
agents: matching,
|
||||||
|
t,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
this.#ensureTick()
|
||||||
|
|
||||||
|
return({
|
||||||
|
ok: true,
|
||||||
|
frequency: frequencyMs,
|
||||||
|
agents: matching,
|
||||||
|
t,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRequestor(id, spec) {
|
||||||
|
const requestor = this.requestors.get(id)
|
||||||
|
if(!requestor) return({ ok: false, err: 'Unknown requestor' })
|
||||||
|
if(Array.isArray(spec?.planes)) {
|
||||||
|
const frustum = Frustum.fromPlanes(spec.planes)
|
||||||
|
if(!frustum) return({ ok: false, err: 'Invalid frustum planes' })
|
||||||
|
requestor.frustum = frustum
|
||||||
|
}
|
||||||
|
if(typeof(spec?.frequency) === 'number') {
|
||||||
|
const frequencyMs = this.#adjustFrequency(spec.frequency)
|
||||||
|
if(frequencyMs === null) return({ ok: false, err: 'Invalid frequency (expected 300–10000 ms)' })
|
||||||
|
requestor.frequencyMs = frequencyMs
|
||||||
|
requestor.pushEveryNTicks = frequencyMs / this.scanIntervalMs
|
||||||
|
requestor.tickCounter = 0
|
||||||
|
}
|
||||||
|
return({ ok: true, frequency: requestor.frequencyMs })
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterRequestor(id) {
|
||||||
|
this.requestors.delete(id)
|
||||||
|
if(this.requestors.size === 0) this.#stopTick()
|
||||||
|
return({ ok: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
getRequestorAgents(id) {
|
||||||
|
const requestor = this.requestors.get(id)
|
||||||
|
if(!requestor) return(null)
|
||||||
|
return({
|
||||||
|
agents: [...requestor.agents],
|
||||||
|
t: requestor.t,
|
||||||
|
frequency: requestor.frequencyMs ?? null,
|
||||||
|
updatedAt: requestor.updatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.requestors.clear()
|
||||||
|
this.#stopTick()
|
||||||
|
}
|
||||||
|
|
||||||
|
async evaluateOnce({ frustum, t }) {
|
||||||
|
if(!frustum || !(frustum instanceof Frustum)) {
|
||||||
|
return({ ok: false, err: 'Invalid frustum', agents: [] })
|
||||||
|
}
|
||||||
|
if(typeof(t) !== 'number' || Number.isNaN(t)) return({ ok: false, err: 'Invalid simulation time', agents: [] })
|
||||||
|
const agents = await this.reader.loadAllAgents()
|
||||||
|
const matching = this.#matchAgents(agents, frustum, t)
|
||||||
|
return({ ok: true, agents: matching, t })
|
||||||
|
}
|
||||||
|
|
||||||
|
#resolveT(requestor) {
|
||||||
|
if(requestor.tMode === 'fixed') return(requestor.fixedT)
|
||||||
|
return(this.getNow())
|
||||||
|
}
|
||||||
|
|
||||||
|
#matchAgents(agents, frustum, t) {
|
||||||
|
const matching = []
|
||||||
|
for(const agent of agents.values()) {
|
||||||
|
const position = positionAt(agent, t)
|
||||||
|
if(!frustum.containsPoint(position)) continue
|
||||||
|
matching.push(this.reader.buildAgentSnapshot(agent, t))
|
||||||
|
}
|
||||||
|
return(matching)
|
||||||
|
}
|
||||||
|
|
||||||
|
#pushUpdate(requestor) {
|
||||||
|
if(typeof(this.onPush) !== 'function') return
|
||||||
|
this.onPush(requestor.id, {
|
||||||
|
agents: [...requestor.agents],
|
||||||
|
t: requestor.t,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async #scanAll() {
|
||||||
|
if(this.#scanInFlight || this.requestors.size === 0) return
|
||||||
|
this.#scanInFlight = true
|
||||||
|
try {
|
||||||
|
const agents = await this.reader.loadAllAgents()
|
||||||
|
for(const requestor of this.requestors.values()) {
|
||||||
|
const t = this.#resolveT(requestor)
|
||||||
|
if(t === null) {
|
||||||
|
requestor.agents = []
|
||||||
|
requestor.t = null
|
||||||
|
requestor.updatedAt = Date.now()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
requestor.agents = this.#matchAgents(agents, requestor.frustum, t)
|
||||||
|
requestor.t = t
|
||||||
|
requestor.updatedAt = Date.now()
|
||||||
|
|
||||||
|
if(!requestor.subscription) continue
|
||||||
|
|
||||||
|
requestor.tickCounter++
|
||||||
|
if(requestor.tickCounter >= requestor.pushEveryNTicks) {
|
||||||
|
requestor.tickCounter = 0
|
||||||
|
this.#pushUpdate(requestor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(this.debug) console.log(`[Observer] Scanned ${agents.size} agent(s) for ${this.requestors.size} requestor(s)`)
|
||||||
|
} catch(err) {
|
||||||
|
console.error('[Observer] Requestor scan failed:', err)
|
||||||
|
} finally {
|
||||||
|
this.#scanInFlight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ensureTick() {
|
||||||
|
if(this.#tickTimer) return
|
||||||
|
this.#tickTimer = setInterval(() => {
|
||||||
|
this.#scanAll()
|
||||||
|
}, this.scanIntervalMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#stopTick() {
|
||||||
|
if(!this.#tickTimer) return
|
||||||
|
clearInterval(this.#tickTimer)
|
||||||
|
this.#tickTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -a
|
||||||
. /etc/p42/secrets.env
|
. /etc/p42/secrets.env
|
||||||
|
set +a
|
||||||
|
|
||||||
daemon=p42Observer
|
daemon=p42Observer
|
||||||
logfile=observer.log
|
logfile=observer.log
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
|
|
||||||
export class ArenaGroom {
|
export class ArenaGroom {
|
||||||
|
|
||||||
constructor(arenaCnx, storage, debug = false) {
|
constructor(arenaCnx, arenaStorage, debug = false) {
|
||||||
this.cnx = arenaCnx
|
this.cnx = arenaCnx
|
||||||
this.storage = storage
|
this.arenaStorage = arenaStorage
|
||||||
this.debug = debug
|
this.debug = debug
|
||||||
}
|
}
|
||||||
|
|
||||||
agentHashKey(agentId) {
|
agentHashKey(agentId) {
|
||||||
return(this.storage.agentHashKey.replace(/\[UID\]/g, agentId))
|
return(this.arenaStorage.agentHashKey.replace(/\[UID\]/g, agentId))
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearArena() {
|
async clearArena() {
|
||||||
const ids = await this.cnx.redisSmembers(this.storage.agentsIndexKey)
|
const ids = await this.cnx.redisSmembers(this.arenaStorage.agentsIndexKey)
|
||||||
for(const id of ids) {
|
for(const id of ids) {
|
||||||
await this.cnx.redisDel(this.agentHashKey(id))
|
await this.cnx.redisDel(this.agentHashKey(id))
|
||||||
}
|
}
|
||||||
await this.cnx.redisDel(this.storage.agentsIndexKey)
|
await this.cnx.redisDel(this.arenaStorage.agentsIndexKey)
|
||||||
if(this.debug) console.log(`[Maestro] Cleared arena store (${ids.length} agent(s))`)
|
if(this.debug) console.log(`[Maestro] Cleared arena store (${ids.length} agent(s))`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export class ArenaGroom {
|
|||||||
const key = this.agentHashKey(agent.id)
|
const key = this.agentHashKey(agent.id)
|
||||||
await this.cnx.redisHset(key, 'position', agent.position)
|
await this.cnx.redisHset(key, 'position', agent.position)
|
||||||
await this.cnx.redisHset(key, 'vector', agent.vector)
|
await this.cnx.redisHset(key, 'vector', agent.vector)
|
||||||
await this.cnx.redisSadd(this.storage.agentsIndexKey, agent.id)
|
await this.cnx.redisSadd(this.arenaStorage.agentsIndexKey, agent.id)
|
||||||
}
|
}
|
||||||
if(this.debug) console.log(`[Maestro] Groomed ${agents.length} agent(s) into arena store`)
|
if(this.debug) console.log(`[Maestro] Groomed ${agents.length} agent(s) into arena store`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -a
|
||||||
. /etc/p42/secrets.env
|
. /etc/p42/secrets.env
|
||||||
|
set +a
|
||||||
|
|
||||||
daemon=p42SimMaestro
|
daemon=p42SimMaestro
|
||||||
logfile=maestro.log
|
logfile=maestro.log
|
||||||
|
|||||||
+4
-2
@@ -12,7 +12,8 @@
|
|||||||
{
|
{
|
||||||
"canDo": [
|
"canDo": [
|
||||||
"GETAGENTPOSITION",
|
"GETAGENTPOSITION",
|
||||||
"GETAGENTSINPRISM"
|
"GETAGENTSINFRUSTUM",
|
||||||
|
"SUBSCRIBEFRUSTUM"
|
||||||
],
|
],
|
||||||
"roles": "*"
|
"roles": "*"
|
||||||
},
|
},
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
"gps": {
|
"gps": {
|
||||||
"gpsActionsChannel": "system:requests:gps",
|
"gpsActionsChannel": "system:requests:gps",
|
||||||
"gpsActionsReply": "system:replies:[UID]",
|
"gpsActionsReply": "system:replies:[UID]",
|
||||||
"storage": {
|
"GPSstorage": {
|
||||||
"agentHashKey": "system:gps:agent:[UID]",
|
"agentHashKey": "system:gps:agent:[UID]",
|
||||||
"agentsIndexKey": "system:gps:agents",
|
"agentsIndexKey": "system:gps:agents",
|
||||||
"positionsStream": "system:gps:positions",
|
"positionsStream": "system:gps:positions",
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
"observerActionsChannel": "system:requests:observer",
|
"observerActionsChannel": "system:requests:observer",
|
||||||
"observerActionsReply": "system:replies:[UID]",
|
"observerActionsReply": "system:replies:[UID]",
|
||||||
"senderId": "observer",
|
"senderId": "observer",
|
||||||
|
"scanIntervalMs": 300,
|
||||||
"lifecycle": {
|
"lifecycle": {
|
||||||
"arenaChannel": "arena:lifecycle",
|
"arenaChannel": "arena:lifecycle",
|
||||||
"godsReadyChannel": "arena:gods:ready"
|
"godsReadyChannel": "arena:gods:ready"
|
||||||
|
|||||||
+3
-2
@@ -62,7 +62,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"gpsActionsChannel": { "type": "string" },
|
"gpsActionsChannel": { "type": "string" },
|
||||||
"gpsActionsReply": { "type": "string" },
|
"gpsActionsReply": { "type": "string" },
|
||||||
"storage": {
|
"GPSstorage": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"agentHashKey": { "type": "string" },
|
"agentHashKey": { "type": "string" },
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"gpsActionsChannel",
|
"gpsActionsChannel",
|
||||||
"gpsActionsReply",
|
"gpsActionsReply",
|
||||||
"storage",
|
"GPSstorage",
|
||||||
"agentVectorChangeChannel",
|
"agentVectorChangeChannel",
|
||||||
"collisionsChannel"
|
"collisionsChannel"
|
||||||
]
|
]
|
||||||
@@ -159,6 +159,7 @@
|
|||||||
"observerActionsChannel": { "type": "string" },
|
"observerActionsChannel": { "type": "string" },
|
||||||
"observerActionsReply": { "type": "string" },
|
"observerActionsReply": { "type": "string" },
|
||||||
"senderId": { "type": "string" },
|
"senderId": { "type": "string" },
|
||||||
|
"scanIntervalMs": { "type": "integer", "minimum": 50 },
|
||||||
"lifecycle": {
|
"lifecycle": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
+4
-2
@@ -1,10 +1,12 @@
|
|||||||
import mysql from 'mysql2/promise'
|
import mysql from 'mysql2/promise'
|
||||||
|
import { loadP42Secrets } from './secretsLoader.js'
|
||||||
|
|
||||||
export function resolveMysqlCredentials(config = {}) {
|
export function resolveMysqlCredentials(config = {}) {
|
||||||
const user = process.env.user
|
loadP42Secrets()
|
||||||
|
const user = process.env.mysql_user
|
||||||
const password = process.env.mysql_pass
|
const password = process.env.mysql_pass
|
||||||
if(!user || !password) {
|
if(!user || !password) {
|
||||||
throw new Error('Missing MySQL credentials: set user and mysql_pass in environment')
|
throw new Error('Missing MySQL credentials: set mysql_user and mysql_pass in environment')
|
||||||
}
|
}
|
||||||
return({
|
return({
|
||||||
socketPath: config.socketPath,
|
socketPath: config.socketPath,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
const DEFAULT_SECRETS_PATH = '/etc/p42/secrets.env'
|
||||||
|
|
||||||
|
function stripQuotes(value) {
|
||||||
|
if(
|
||||||
|
(value.startsWith('"') && value.endsWith('"')) ||
|
||||||
|
(value.startsWith("'") && value.endsWith("'"))
|
||||||
|
) {
|
||||||
|
return(value.slice(1, -1))
|
||||||
|
}
|
||||||
|
return(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadP42Secrets(filePath = DEFAULT_SECRETS_PATH) {
|
||||||
|
if(process.env.mysql_user && process.env.mysql_pass) return(true)
|
||||||
|
|
||||||
|
if(!fs.existsSync(filePath)) return(false)
|
||||||
|
|
||||||
|
const text = fs.readFileSync(filePath, 'utf8')
|
||||||
|
for(const rawLine of text.split('\n')) {
|
||||||
|
const line = rawLine.trim()
|
||||||
|
if(!line || line.startsWith('#')) continue
|
||||||
|
const eq = line.indexOf('=')
|
||||||
|
if(eq < 1) continue
|
||||||
|
const key = line.slice(0, eq).trim()
|
||||||
|
const value = stripQuotes(line.slice(eq + 1).trim())
|
||||||
|
if(key === 'mysql_user' || key === 'mysql_pass') {
|
||||||
|
process.env[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return(Boolean(process.env.mysql_user && process.env.mysql_pass))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user