openvibers/openvibe.games Commits
Map: answer the second built-in place with its own map, not a 404 (#283)
Changes
@@@ -69,6 +69,23 @@ describe('loadConfig surf place', () => {})})++describe('loadConfig editor maps', () => {++ it('gives the other built-in place a sibling map document, never a 404', () => {++ const scraplandia = loadConfig({++ GAMES_PLACE_ID: 'scraplandia',++ MAP_PATH: '/opt/openvibe.games/data/map.json',++ })++ expect(scraplandia.mapPath).toBe('/opt/openvibe.games/data/map.json')++ expect(scraplandia.secondaryMapPath).toBe('/opt/openvibe.games/data/salvage-rush-map.json')++++ const rush = loadConfig({++ GAMES_PLACE_ID: 'salvage-rush',++ MAP_PATH: '/opt/openvibe.games/data/salvage-rush-map.json',++ })++ expect(rush.secondaryMapPath).toBe('/opt/openvibe.games/data/map.json')++ })++})++describe('loadConfig scriptMods', () => {it('is off unless the place enables it', () => {expect(loadConfig({}).scriptMods).toBe(false)@@@ -1,3 +1,4 @@++import { dirname, join } from 'node:path'import { surfMap } from '@openvibe/content'import { MIGRATIONS_DIR } from '@openvibe/persistence'import { SIM_TICK_RATE, SURF_SNAPSHOT_EVERY, SURF_TICK_RATE } from '@openvibe/shared'@@@ -30,6 +31,11 @@ export interface ServerConfig {/** Directory of built client assets to serve, or null for API/WS only. */staticDir: string | nullmapPath: string++ /**++ * The editor map of the other built-in place this process runs (main.ts serves both): a sibling of `mapPath` named++ * after its place. A missing file is an empty map, not a 404, so the client never reports a failed map request.++ */++ secondaryMapPath: string/*** TRUST_PROXY: which proxies may set the client-address headers. null = none* (the TCP peer is the client). 'all' = the server is only reachable through@@@ -209,6 +215,14 @@ export function loadConfig(env: NodeJS.ProcessEnv): ServerConfig {// A surf deployment's place id is its map id; the old `surf-lagoon` env value// boots the canonical place rather than starting a second, aliased one.const placeId = surfMap(requestedPlace)?.id ?? requestedPlace++ const mapPath =++ env.MAP_PATH ?? (placeId === 'salvage-rush' ? 'data/salvage-rush-map.json' : 'data/map.json')++ // main.ts also runs the other built-in place in this process. Its map document is the sibling of the primary one;++ // the file not existing yet is an empty map (mapStore.loadMap), where a 404 would show up as a failed request.++ const secondaryMapPath = join(++ dirname(mapPath),++ placeId === 'salvage-rush' ? 'map.json' : 'salvage-rush-map.json',++ )return {port: intEnv(env, 'PORT', 8000),host: env.HOST ?? '0.0.0.0',@@@ -223,9 +237,8 @@ export function loadConfig(env: NodeJS.ProcessEnv): ServerConfig {valkeyPrefix: env.VALKEY_PREFIX ?? 'ov:games:',},staticDir: env.STATIC_DIR ?? null,-- mapPath:-- env.MAP_PATH ??-- (placeId === 'salvage-rush' ? 'data/salvage-rush-map.json' : 'data/map.json'),++ mapPath,++ secondaryMapPath,trustProxy: parseProxyTrust(env.TRUST_PROXY),/** openvibe.network auth endpoint; when set, editor tokens validate there. */networkAuthUrl:@@@ -1356,13 +1356,14 @@ async function main(): Promise<void> {log.child({ system: 'indexnow' }),),config.placeId,-- // One map document per place: surf maps are separate files, the rest-- // share the configured editor map.++ // One map document per place: surf maps are separate files, each built-in place has its own document (an absent++ // file is an empty map, not a 404), and anything else has no map.(place) => {const surf = place ? surfMapOfPlace(place) : nullif (surf) return surfMapPath(surf.id)if (!place || place === config.placeId)return primarySurf ? surfMapPath(primarySurf.id) : config.mapPath++ if (place === secondaryPlace) return config.secondaryMapPathreturn undefined},)@@@ -87,6 +87,9 @@ beforeAll(async () => {writeFileSync(join(dist, 'assets', 'tex', 'wood_planks.jpg'), 'jpeg')writeFileSync(join(dist, 'assets', 'tex', 'wood_planks.ktx2'), 'ktx2')const mapPath = join(dir, 'data', 'map.json')++ // The process runs both built-in places. This one has no file yet: /map.json?game=salvage-rush must still be an++ // empty map document, not the 404 the play client logged (main.ts's mapPathFor answers it from here).++ const secondaryMapPath = join(dir, 'data', 'salvage-rush-map.json')mkdirSync(join(dir, 'data', 'map-assets'), { recursive: true })writeFileSync(join(dir, 'data', 'map-assets', 'tex-1700000000000.png'), 'png')@@@ -108,6 +111,14 @@ beforeAll(async () => {async () => [{ slot: 0, name: 'Ana', appearance: null }],null,{ handle: (req, res) => readiness.handle(req, res) },++ undefined,++ undefined,++ 'scraplandia',++ (place) => {++ if (!place || place === 'scraplandia') return mapPath++ if (place === 'salvage-rush') return secondaryMapPath++ return undefined++ },)bare = createHttpServer(null, metrics, log)port = await listen(server)@@@ -167,6 +178,17 @@ describe('real routes on the apex', () => {expect(tex.headers['content-type']).toBe('image/png')})++ it('answers the other built-in place with an empty map, not a 404', async () => {++ const map = await send('/map.json?game=salvage-rush')++ expect(map.status).toBe(200)++ expect(map.headers['content-type']).toBe('application/json')++ expect(map.headers.etag).toBeTruthy()++ const body = JSON.parse(map.body) as { v?: number }++ expect(body.v).toBe(2)++ // A place this process does not run still has no map.++ expect((await send('/map.json?game=not-a-place')).status).toBe(404)++ })++it('answers the SSO routes', async () => {const me = await send('/auth/me')expect(me.status).toBe(200)
@@ -69,6 +69,23 @@ describe('loadConfig surf place', () => {
@@ -69,6 +69,23 @@ describe('loadConfig surf place', () => {
})
})
})
})
+describe('loadConfig editor maps', () => {
+ it('gives the other built-in place a sibling map document, never a 404', () => {
+ const scraplandia = loadConfig({
+ GAMES_PLACE_ID: 'scraplandia',
+ MAP_PATH: '/opt/openvibe.games/data/map.json',
+ })
+ expect(scraplandia.mapPath).toBe('/opt/openvibe.games/data/map.json')
+ expect(scraplandia.secondaryMapPath).toBe('/opt/openvibe.games/data/salvage-rush-map.json')
+
+ const rush = loadConfig({
+ GAMES_PLACE_ID: 'salvage-rush',
+ MAP_PATH: '/opt/openvibe.games/data/salvage-rush-map.json',
+ })
+ expect(rush.secondaryMapPath).toBe('/opt/openvibe.games/data/map.json')
+ })
+})
+
describe('loadConfig scriptMods', () => {
describe('loadConfig scriptMods', () => {
it('is off unless the place enables it', () => {
it('is off unless the place enables it', () => {
expect(loadConfig({}).scriptMods).toBe(false)
expect(loadConfig({}).scriptMods).toBe(false)
@@ -1,3 +1,4 @@
@@ -1,3 +1,4 @@
+import { dirname, join } from 'node:path'
import { surfMap } from '@openvibe/content'
import { surfMap } from '@openvibe/content'
import { MIGRATIONS_DIR } from '@openvibe/persistence'
import { MIGRATIONS_DIR } from '@openvibe/persistence'
import { SIM_TICK_RATE, SURF_SNAPSHOT_EVERY, SURF_TICK_RATE } from '@openvibe/shared'
import { SIM_TICK_RATE, SURF_SNAPSHOT_EVERY, SURF_TICK_RATE } from '@openvibe/shared'
@@ -30,6 +31,11 @@ export interface ServerConfig {
@@ -30,6 +31,11 @@ export interface ServerConfig {
/** Directory of built client assets to serve, or null for API/WS only. */
/** Directory of built client assets to serve, or null for API/WS only. */
staticDir: string | null
staticDir: string | null
mapPath: string
mapPath: string
+ /**
+ * The editor map of the other built-in place this process runs (main.ts serves both): a sibling of `mapPath` named
+ * after its place. A missing file is an empty map, not a 404, so the client never reports a failed map request.
+ */
+ secondaryMapPath: string
/**
/**
* TRUST_PROXY: which proxies may set the client-address headers. null = none
* TRUST_PROXY: which proxies may set the client-address headers. null = none
* (the TCP peer is the client). 'all' = the server is only reachable through
* (the TCP peer is the client). 'all' = the server is only reachable through
@@ -209,6 +215,14 @@ export function loadConfig(env: NodeJS.ProcessEnv): ServerConfig {
@@ -209,6 +215,14 @@ export function loadConfig(env: NodeJS.ProcessEnv): ServerConfig {
// A surf deployment's place id is its map id; the old `surf-lagoon` env value
// A surf deployment's place id is its map id; the old `surf-lagoon` env value
// boots the canonical place rather than starting a second, aliased one.
// boots the canonical place rather than starting a second, aliased one.
const placeId = surfMap(requestedPlace)?.id ?? requestedPlace
const placeId = surfMap(requestedPlace)?.id ?? requestedPlace
+ const mapPath =
+ env.MAP_PATH ?? (placeId === 'salvage-rush' ? 'data/salvage-rush-map.json' : 'data/map.json')
+ // main.ts also runs the other built-in place in this process. Its map document is the sibling of the primary one;
+ // the file not existing yet is an empty map (mapStore.loadMap), where a 404 would show up as a failed request.
+ const secondaryMapPath = join(
+ dirname(mapPath),
+ placeId === 'salvage-rush' ? 'map.json' : 'salvage-rush-map.json',
+ )
return {
return {
port: intEnv(env, 'PORT', 8000),
port: intEnv(env, 'PORT', 8000),
host: env.HOST ?? '0.0.0.0',
host: env.HOST ?? '0.0.0.0',
@@ -223,9 +237,8 @@ export function loadConfig(env: NodeJS.ProcessEnv): ServerConfig {
@@ -223,9 +237,8 @@ export function loadConfig(env: NodeJS.ProcessEnv): ServerConfig {
valkeyPrefix: env.VALKEY_PREFIX ?? 'ov:games:',
valkeyPrefix: env.VALKEY_PREFIX ?? 'ov:games:',
},
},
staticDir: env.STATIC_DIR ?? null,
staticDir: env.STATIC_DIR ?? null,
- mapPath:
- env.MAP_PATH ??
- (placeId === 'salvage-rush' ? 'data/salvage-rush-map.json' : 'data/map.json'),
+ mapPath,
+ secondaryMapPath,
trustProxy: parseProxyTrust(env.TRUST_PROXY),
trustProxy: parseProxyTrust(env.TRUST_PROXY),
/** openvibe.network auth endpoint; when set, editor tokens validate there. */
/** openvibe.network auth endpoint; when set, editor tokens validate there. */
networkAuthUrl:
networkAuthUrl:
@@ -1356,13 +1356,14 @@ async function main(): Promise<void> {
@@ -1356,13 +1356,14 @@ async function main(): Promise<void> {
log.child({ system: 'indexnow' }),
log.child({ system: 'indexnow' }),
),
),
config.placeId,
config.placeId,
- // One map document per place: surf maps are separate files, the rest
- // share the configured editor map.
+ // One map document per place: surf maps are separate files, each built-in place has its own document (an absent
+ // file is an empty map, not a 404), and anything else has no map.
(place) => {
(place) => {
const surf = place ? surfMapOfPlace(place) : null
const surf = place ? surfMapOfPlace(place) : null
if (surf) return surfMapPath(surf.id)
if (surf) return surfMapPath(surf.id)
if (!place || place === config.placeId)
if (!place || place === config.placeId)
return primarySurf ? surfMapPath(primarySurf.id) : config.mapPath
return primarySurf ? surfMapPath(primarySurf.id) : config.mapPath
+ if (place === secondaryPlace) return config.secondaryMapPath
return undefined
return undefined
},
},
)
)
@@ -87,6 +87,9 @@ beforeAll(async () => {
@@ -87,6 +87,9 @@ beforeAll(async () => {
writeFileSync(join(dist, 'assets', 'tex', 'wood_planks.jpg'), 'jpeg')
writeFileSync(join(dist, 'assets', 'tex', 'wood_planks.jpg'), 'jpeg')
writeFileSync(join(dist, 'assets', 'tex', 'wood_planks.ktx2'), 'ktx2')
writeFileSync(join(dist, 'assets', 'tex', 'wood_planks.ktx2'), 'ktx2')
const mapPath = join(dir, 'data', 'map.json')
const mapPath = join(dir, 'data', 'map.json')
+ // The process runs both built-in places. This one has no file yet: /map.json?game=salvage-rush must still be an
+ // empty map document, not the 404 the play client logged (main.ts's mapPathFor answers it from here).
+ const secondaryMapPath = join(dir, 'data', 'salvage-rush-map.json')
mkdirSync(join(dir, 'data', 'map-assets'), { recursive: true })
mkdirSync(join(dir, 'data', 'map-assets'), { recursive: true })
writeFileSync(join(dir, 'data', 'map-assets', 'tex-1700000000000.png'), 'png')
writeFileSync(join(dir, 'data', 'map-assets', 'tex-1700000000000.png'), 'png')
@@ -108,6 +111,14 @@ beforeAll(async () => {
@@ -108,6 +111,14 @@ beforeAll(async () => {
async () => [{ slot: 0, name: 'Ana', appearance: null }],
async () => [{ slot: 0, name: 'Ana', appearance: null }],
null,
null,
{ handle: (req, res) => readiness.handle(req, res) },
{ handle: (req, res) => readiness.handle(req, res) },
+ undefined,
+ undefined,
+ 'scraplandia',
+ (place) => {
+ if (!place || place === 'scraplandia') return mapPath
+ if (place === 'salvage-rush') return secondaryMapPath
+ return undefined
+ },
)
)
bare = createHttpServer(null, metrics, log)
bare = createHttpServer(null, metrics, log)
port = await listen(server)
port = await listen(server)
@@ -167,6 +178,17 @@ describe('real routes on the apex', () => {
@@ -167,6 +178,17 @@ describe('real routes on the apex', () => {
expect(tex.headers['content-type']).toBe('image/png')
expect(tex.headers['content-type']).toBe('image/png')
})
})
+ it('answers the other built-in place with an empty map, not a 404', async () => {
+ const map = await send('/map.json?game=salvage-rush')
+ expect(map.status).toBe(200)
+ expect(map.headers['content-type']).toBe('application/json')
+ expect(map.headers.etag).toBeTruthy()
+ const body = JSON.parse(map.body) as { v?: number }
+ expect(body.v).toBe(2)
+ // A place this process does not run still has no map.
+ expect((await send('/map.json?game=not-a-place')).status).toBe(404)
+ })
+
it('answers the SSO routes', async () => {
it('answers the SSO routes', async () => {
const me = await send('/auth/me')
const me = await send('/auth/me')
expect(me.status).toBe(200)
expect(me.status).toBe(200)