@underpostnet/cyberia 3.2.80 → 3.2.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +34 -15
- package/.github/workflows/cyberia-client.cd.yml +13 -1
- package/.github/workflows/cyberia-server.cd.yml +13 -1
- package/.github/workflows/docker-image.cyberia-client.ci.yml +4 -4
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +4 -4
- package/.github/workflows/docker-image.cyberia-server.ci.yml +4 -4
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +4 -4
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +3 -3
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +3 -3
- package/.github/workflows/engine-cyberia.cd.yml +14 -3
- package/CHANGELOG.md +182 -1
- package/CLI-HELP.md +37 -16
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +1 -1
- package/bin/cyberia.js +203 -49
- package/bin/deploy.js +18 -16
- package/bin/index.js +203 -49
- package/compose.env +34 -15
- package/deployment.yaml +1 -210
- package/docker-compose.yml +73 -62
- package/hardhat/package-lock.json +134 -126
- package/hardhat/package.json +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -210
- package/manifests/deployment/dd-cyberia-development/gateway.yaml +80 -0
- package/manifests/deployment/dd-cyberia-development/httproute.yaml +504 -0
- package/manifests/deployment/dd-cyberia-development/proxy.yaml +12 -12
- package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +0 -82
- package/manifests/deployment/dd-cyberia-development/traffic-service.yaml +121 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/manifests/mongodb/kustomization.yaml +4 -1
- package/manifests/mongodb/statefulset.yaml +4 -0
- package/manifests/mongodb/storage-class.yaml +9 -2
- package/nginx.conf +86 -16
- package/package.json +2 -2
- package/proxy.yaml +12 -12
- package/pv-pvc.yaml +0 -82
- package/scripts/nat-iptables.sh +10 -4
- package/scripts/test-monitor.sh +4 -3
- package/src/api/cyberia-action/cyberia-action.model.js +1 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +41 -14
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +8 -12
- package/src/api/cyberia-instance/cyberia-portal-connector.js +7 -5
- package/src/api/cyberia-instance/cyberia-random-source.js +80 -0
- package/src/api/cyberia-instance/cyberia-world-generator.js +4 -3
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +73 -0
- package/src/cli/cluster.js +740 -55
- package/src/cli/db.js +2 -2
- package/src/cli/deploy.js +1679 -174
- package/src/cli/docker-compose.js +19 -178
- package/src/cli/image.js +15 -6
- package/src/cli/index.js +124 -35
- package/src/cli/ipfs.js +82 -11
- package/src/cli/monitor.js +1 -1
- package/src/cli/repository.js +1 -1
- package/src/cli/run.js +2161 -420
- package/src/cli/secrets.js +969 -0
- package/src/cli/ssh.js +8 -28
- package/src/client/components/cyberia/InstanceSelectionView.js +11 -8
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +5 -0
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +106 -39
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +18 -0
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +44 -7
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client-builder/client-build.js +94 -11
- package/src/client-builder/ssr.js +27 -73
- package/src/db/mongo/MongoBootstrap.js +295 -54
- package/src/db/mongo/MongooseDB.js +47 -32
- package/src/index.js +1 -1
- package/src/projects/cyberia/besu-genesis-generator.js +3 -2
- package/src/projects/cyberia/hot-reload-trigger.js +3 -3
- package/src/projects/cyberia/instance-data.js +42 -1
- package/src/runtime/cyberia-client/Dockerfile +1 -1
- package/src/runtime/cyberia-client/Dockerfile.dev +1 -1
- package/src/runtime/cyberia-server/Dockerfile +1 -1
- package/src/runtime/cyberia-server/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
- package/src/runtime/engine-cyberia/compose.env +34 -15
- package/src/runtime/engine-cyberia/docker-compose.yml +73 -62
- package/src/runtime/engine-cyberia/nginx.conf +86 -16
- package/src/server/conf.js +1208 -70
- package/src/server/cri.js +70 -0
- package/src/server/underpost-gateway.js +1073 -0
- package/src/server/underpost-ingress.js +364 -0
- package/test/cluster-instances.test.js +435 -0
- package/test/deploy-node-placement.test.js +45 -0
- package/test/instance-traffic-plan.test.js +710 -0
- package/test/sops-secret-store.test.js +612 -0
- package/test/underpost-gateway.test.js +469 -0
- package/test/underpost-ingress.test.js +253 -0
package/src/cli/ssh.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { generateRandomPasswordSelection } from '../client/components/core/CommonJs.js';
|
|
8
8
|
import { pbcopy, shellExec } from '../server/process.js';
|
|
9
9
|
import { loggerFactory } from '../server/logger.js';
|
|
10
|
+
import { waitForPort } from '../server/conf.js';
|
|
10
11
|
import fs from 'fs-extra';
|
|
11
12
|
import Underpost from '../index.js';
|
|
12
13
|
|
|
@@ -606,8 +607,8 @@ EOF
|
|
|
606
607
|
},
|
|
607
608
|
|
|
608
609
|
/**
|
|
609
|
-
* Waits until a TCP SSH port becomes reachable on a host.
|
|
610
|
-
* @
|
|
610
|
+
* Waits until a TCP SSH port becomes reachable on a host. Delegates to
|
|
611
|
+
* {@link ServerConfBuilder.waitForPort}, which owns the probe.
|
|
611
612
|
* @function waitForSshPort
|
|
612
613
|
* @memberof UnderpostSSH
|
|
613
614
|
* @param {object} params
|
|
@@ -617,25 +618,14 @@ EOF
|
|
|
617
618
|
* @param {number} [params.intervalMs=3000] - Poll interval.
|
|
618
619
|
* @returns {Promise<boolean>} True once the port accepts connections, false on timeout.
|
|
619
620
|
*/
|
|
620
|
-
waitForSshPort:
|
|
621
|
-
|
|
622
|
-
while (Date.now() < deadline) {
|
|
623
|
-
const probe = shellExec(
|
|
624
|
-
`timeout 5 bash -c '</dev/tcp/${host}/${port}' >/dev/null 2>&1 && echo open || echo closed`,
|
|
625
|
-
{ silent: true, stdout: true, silentOnError: true, disableLog: true },
|
|
626
|
-
);
|
|
627
|
-
if (`${probe}`.trim() === 'open') return true;
|
|
628
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
629
|
-
}
|
|
630
|
-
logger.warn(`SSH port ${host}:${port} not reachable within timeout`);
|
|
631
|
-
return false;
|
|
632
|
-
},
|
|
621
|
+
waitForSshPort: ({ host, port = 22, timeoutMs = 10 * 60 * 1000, intervalMs = 3000 }) =>
|
|
622
|
+
waitForPort({ host, port, open: true, timeoutMs, intervalMs }),
|
|
633
623
|
|
|
634
624
|
/**
|
|
635
625
|
* Waits until a host's SSH port stops accepting connections (e.g. while it
|
|
636
626
|
* reboots). Used to detect a reboot edge before waiting for the port to come
|
|
637
627
|
* back up, so callers don't latch onto the pre-reboot (ephemeral) sshd.
|
|
638
|
-
* @
|
|
628
|
+
* Delegates to {@link ServerConfBuilder.waitForPort}.
|
|
639
629
|
* @function waitForSshPortClosed
|
|
640
630
|
* @memberof UnderpostSSH
|
|
641
631
|
* @param {object} params
|
|
@@ -645,18 +635,8 @@ EOF
|
|
|
645
635
|
* @param {number} [params.intervalMs=3000] - Poll interval.
|
|
646
636
|
* @returns {Promise<boolean>} True once the port is closed, false on timeout.
|
|
647
637
|
*/
|
|
648
|
-
waitForSshPortClosed:
|
|
649
|
-
|
|
650
|
-
while (Date.now() < deadline) {
|
|
651
|
-
const probe = shellExec(
|
|
652
|
-
`timeout 5 bash -c '</dev/tcp/${host}/${port}' >/dev/null 2>&1 && echo open || echo closed`,
|
|
653
|
-
{ silent: true, stdout: true, silentOnError: true, disableLog: true },
|
|
654
|
-
);
|
|
655
|
-
if (`${probe}`.trim() === 'closed') return true;
|
|
656
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
657
|
-
}
|
|
658
|
-
return false;
|
|
659
|
-
},
|
|
638
|
+
waitForSshPortClosed: ({ host, port = 22, timeoutMs = 3 * 60 * 1000, intervalMs = 3000 }) =>
|
|
639
|
+
waitForPort({ host, port, open: false, timeoutMs, intervalMs }),
|
|
660
640
|
|
|
661
641
|
/**
|
|
662
642
|
* Orchestrates a non-interactive, key-only SSH session against a freshly
|
|
@@ -26,6 +26,7 @@ import { htmls, s, htmlStrSanitize } from '../core/VanillaJs.js';
|
|
|
26
26
|
import { getProxyPath } from '../core/Router.js';
|
|
27
27
|
import { getApiBaseUrl } from '../../services/core/core.service.js';
|
|
28
28
|
import { CyberiaInstanceService } from '../../services/cyberia-instance/cyberia-instance.service.js';
|
|
29
|
+
import { DEFAULT_INSTANCE_CODE } from './SharedDefaultsCyberia.js';
|
|
29
30
|
|
|
30
31
|
// Client deployment that actually hosts the playable worlds. Matches the portal
|
|
31
32
|
// landing CTA (MainBodyCyberiaPortal); override per-instance via `onPlay`.
|
|
@@ -33,9 +34,8 @@ const DEFAULT_CLIENT_BASE_URL = 'https://client.cyberiaonline.com';
|
|
|
33
34
|
|
|
34
35
|
// Authoritative simulation host and the default instance's code, mirroring the
|
|
35
36
|
// mmo-server multiInstance block in conf.instances.json. The default variant is
|
|
36
|
-
// served at
|
|
37
|
+
// served at `/`; other variants use their literal `/<CODE>` path.
|
|
37
38
|
const DEFAULT_SERVER_BASE_URL = 'https://server.cyberiaonline.com';
|
|
38
|
-
const DEFAULT_INSTANCE_CODE = 'amethyst-strata-expansion';
|
|
39
39
|
|
|
40
40
|
const placeholderThumbnail = () => `${getProxyPath()}assets/ui-icons/world-default-forest-city.png`;
|
|
41
41
|
|
|
@@ -95,17 +95,17 @@ const normalizeInstance = (doc, { clientBaseUrl }) => {
|
|
|
95
95
|
players: Number.isFinite(players) ? players : null,
|
|
96
96
|
capacity: Number.isFinite(capacity) && capacity > 0 ? capacity : null,
|
|
97
97
|
tags: Array.isArray(doc.tags) ? doc.tags.filter(Boolean) : [],
|
|
98
|
-
playUrl: `${clientBaseUrl}${code ? `/${encodeURIComponent(code)}` : ''}`,
|
|
98
|
+
playUrl: `${clientBaseUrl}${code && code !== DEFAULT_INSTANCE_CODE ? `/${encodeURIComponent(code)}` : ''}`,
|
|
99
99
|
playable: statusMeta(status).playable,
|
|
100
100
|
};
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
// ── Live status via the cyberia-server metrics API ───────────────────────────
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
104
|
+
// The default is served at `/`; other variants use the compact `/<CODE>` paths
|
|
105
|
+
// from conf.instances.json. The proxy strips non-root prefixes, so each world's
|
|
106
|
+
// own /api/v1/metrics reports its health + capacity.
|
|
107
|
+
// Cross-origin, so the metrics CORS allow-list (CYBERIA_CORS_ALLOWED_ORIGINS)
|
|
108
|
+
// must include this portal.
|
|
109
109
|
|
|
110
110
|
const metricsUrlFor = (code, { serverBaseUrl, defaultInstanceCode }) => {
|
|
111
111
|
const path = code && code !== defaultInstanceCode ? `/${encodeURIComponent(code)}` : '';
|
|
@@ -316,6 +316,9 @@ class InstanceSelectionView {
|
|
|
316
316
|
});
|
|
317
317
|
return;
|
|
318
318
|
}
|
|
319
|
+
const defaultSuffix = `/${encodeURIComponent(DEFAULT_INSTANCE_CODE)}`;
|
|
320
|
+
if (instance.playUrl?.endsWith(defaultSuffix))
|
|
321
|
+
instance.playUrl = instance.playUrl.slice(0, -defaultSuffix.length);
|
|
319
322
|
if (typeof onPlay === 'function') return onPlay(instance);
|
|
320
323
|
if (instance.playUrl) location.href = instance.playUrl;
|
|
321
324
|
};
|
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
// Shared content vocabulary
|
|
51
51
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Default instance code for the Cyberia engine
|
|
55
|
+
*/
|
|
56
|
+
export const DEFAULT_INSTANCE_CODE = 'amethyst-strata-expansion';
|
|
57
|
+
|
|
53
58
|
/**
|
|
54
59
|
* Canonical set of ObjectLayer item type names. Used as the
|
|
55
60
|
* `data.item.type` discriminator and as the asset directory name on disk.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
The Action System defines how NPC entities interact with players. An **Action** is a spatial, typed payload attached to a map entity that the player activates by tapping the NPC. Actions drive dialogue, shops, crafting, storage, and quest grant events.
|
|
10
10
|
|
|
11
|
-
> **Implementation status — Alpha (talk / quest-talk):** The CyberiaAction and CyberiaDialogue MongoDB schemas and Engine REST API (`src/api/cyberia-action`, `src/api/cyberia-dialogue`) are defined. The `talk` and `quest-talk` paths are wired end-to-end: the Go server binds actions to entities at instance init, validates dialogue completion, grants quests, and advances `talk` objectives (see **Dialogue Interaction Protocol** below). Shop
|
|
11
|
+
> **Implementation status — Alpha (talk / quest-talk / shop):** The CyberiaAction and CyberiaDialogue MongoDB schemas and Engine REST API (`src/api/cyberia-action`, `src/api/cyberia-dialogue`) are defined. The `talk` and `quest-talk` paths are wired end-to-end: the Go server binds actions to entities at instance init, validates dialogue completion, grants quests, and advances `talk` objectives (see **Dialogue Interaction Protocol** below). The `shop` path is wired end-to-end as well (see **Shop Transaction Flow**). Craft / storage transaction processing remains planned for a later Alpha increment. The `freeze_start`/`freeze_end` WS messages for modal protection are implemented; dialogue freeze now rides on the `dlg_*` frames.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -18,9 +18,9 @@ The Action System defines how NPC entities interact with players. An **Action**
|
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
CyberiaAction {
|
|
21
|
-
code: String // stable unique slug
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
code: String // stable unique, location-scoped slug
|
|
22
|
+
label: String // NPC overhead nameplate (there is no `type` field —
|
|
23
|
+
// see Action Capabilities below)
|
|
24
24
|
|
|
25
25
|
// Spatial origin — NPC entity cell providing this action
|
|
26
26
|
sourceMapCode: String
|
|
@@ -50,7 +50,7 @@ CyberiaAction {
|
|
|
50
50
|
ingredients: [{ itemId: String, qty: Number }]
|
|
51
51
|
}]
|
|
52
52
|
|
|
53
|
-
storageSlots: Number //
|
|
53
|
+
storageSlots: Number // vault capacity in slots; 0 disables the capability
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -70,15 +70,19 @@ A single `code` groups many ordered dialogue lines. The C client fetches all lin
|
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
## Action
|
|
73
|
+
## Action Capabilities
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| `
|
|
75
|
+
An action has **no type**. Its capabilities are whatever payloads are populated,
|
|
76
|
+
resolved per player at interaction time — one action can be a shop and a
|
|
77
|
+
quest-talk giver at once.
|
|
78
|
+
|
|
79
|
+
| Capability | Active when | Payload |
|
|
80
|
+
| ------------ | --------------------------------------------------------------- | ---------------------------------------------------- |
|
|
81
|
+
| `quest-talk` | CyberiaQuests are bound to this action's cell | quests bound by cell, `dialogCode`, `questDialogueCodes` |
|
|
82
|
+
| `talk` | always — satisfies `talk` quest objectives | `dialogCode`, `questDialogueCodes` |
|
|
83
|
+
| `shop` | `shopItems[]` is non-empty — player buys items with a currency | `shopItems[]` |
|
|
84
|
+
| `craft` | `craftRecipes[]` is non-empty — player assembles outputs | `craftRecipes[]` |
|
|
85
|
+
| `storage` | `storageSlots > 0` — player banks items in a personal vault | `storageSlots` |
|
|
82
86
|
|
|
83
87
|
---
|
|
84
88
|
|
|
@@ -109,26 +113,58 @@ graph LR
|
|
|
109
113
|
|
|
110
114
|
## Shop Transaction Flow
|
|
111
115
|
|
|
116
|
+
An action carrying a non-empty `shopItems[]` is a **vendor** — there is no type
|
|
117
|
+
flag. The catalog reaches the two runtimes on their own transports: the Go
|
|
118
|
+
server receives it with the world over gRPC (`CyberiaActionMessage.shop_items`),
|
|
119
|
+
the C client fetches it by action code over REST and renders the **Shop** tab.
|
|
120
|
+
Only the server prices a purchase.
|
|
121
|
+
|
|
122
|
+
A live vendor also lights the **action-provider** capability bit
|
|
123
|
+
(`InteractionFlagAction`), so it carries the same overhead attention icon,
|
|
124
|
+
orbiting particles, and coloured interaction-column border as a pending
|
|
125
|
+
action-talk — the player can see there is something to do before tapping.
|
|
126
|
+
|
|
112
127
|
```mermaid
|
|
113
128
|
sequenceDiagram
|
|
114
|
-
participant P as Player
|
|
129
|
+
participant P as Player (C client)
|
|
115
130
|
participant G as Go Server
|
|
116
131
|
participant E as Engine (Node.js)
|
|
117
132
|
|
|
118
|
-
|
|
119
|
-
G
|
|
120
|
-
E
|
|
121
|
-
|
|
122
|
-
P
|
|
123
|
-
|
|
124
|
-
P->>
|
|
125
|
-
G
|
|
126
|
-
Note over G:
|
|
127
|
-
G->>
|
|
128
|
-
G
|
|
129
|
-
G
|
|
133
|
+
E-->>G: getFullInstance → CyberiaAction { shopItems: [...] } (world build)
|
|
134
|
+
G-->>P: AOI bot block → actionCode + action-provider capability bit
|
|
135
|
+
P->>E: GET /api/cyberia-action/code/:code (interaction modal opens)
|
|
136
|
+
E-->>P: CyberiaAction { label, dialogCode, questDialogueCodes, shopItems }
|
|
137
|
+
Note over P: Shop tab leads the strip and opens active.<br/>Two columns of cards: item slot, name,<br/>price icon + qty, Buy (wallet icon)
|
|
138
|
+
|
|
139
|
+
P->>P: Buy → quantity picker (◀ / ▶ #N, 1..10, capped by what the player<br/>can pay) with a running total, then Cancel or Buy
|
|
140
|
+
P->>G: shop_buy { entityId, itemId, quantity }
|
|
141
|
+
Note over G: vendor bound to entity? row on sale?<br/>entity inside the player's AOI?<br/>held(priceItemId) >= priceQty × quantity?
|
|
142
|
+
G->>G: FreezePlayer("interact") — no kill mid-trade
|
|
143
|
+
G->>G: removePlayerItem(priceItemId, priceQty × quantity)
|
|
144
|
+
G->>G: addPlayerItem(itemId, quantity) + collect-objective reconcile
|
|
145
|
+
G-->>P: shop_ack { entityId, itemId, quantity, ok, reason }
|
|
146
|
+
G-->>P: AOI self-player block → authoritative inventory
|
|
147
|
+
Note over P: the card holds until the grant lands, then the<br/>currency's "-N" pop + expend spray play, and the<br/>item flies from the picker's slot into its<br/>inventory slot ("+N" pop)
|
|
130
148
|
```
|
|
131
149
|
|
|
150
|
+
The picker deliberately waits for the grant before animating: a first copy has
|
|
151
|
+
no inventory slot until the server delivers it, so launching the flight on the
|
|
152
|
+
button press would aim at the bar's fallback centre instead of the item's own
|
|
153
|
+
slot. Waiting also fixes the ordering — the spend is seen leaving before the
|
|
154
|
+
goods arrive.
|
|
155
|
+
|
|
156
|
+
Binary uplink opcode: `shop_buy` `0x1C` — `[u8 kind][str entityId][str itemId][u8 quantity]`.
|
|
157
|
+
The quantity is clamped server-side to `[1, shopBuyMaxQty]` (10); a client that
|
|
158
|
+
sends 0 means one unit. A purchase is all-or-nothing: an unaffordable total is
|
|
159
|
+
rejected rather than partially filled.
|
|
160
|
+
|
|
161
|
+
Rejection reasons on `shop_ack`: `no_vendor`, `not_for_sale`, `out_of_range`,
|
|
162
|
+
`insufficient_funds`.
|
|
163
|
+
|
|
164
|
+
`shop_ack` is notify-only, and only a rejection is surfaced (as a toast). The
|
|
165
|
+
inventory itself always arrives through the AOI self-player block, so a dropped
|
|
166
|
+
ack costs the player nothing.
|
|
167
|
+
|
|
132
168
|
---
|
|
133
169
|
|
|
134
170
|
## Craft Transaction Flow
|
|
@@ -159,11 +195,18 @@ sequenceDiagram
|
|
|
159
195
|
## Dialogue Interaction Protocol (talk / quest-talk)
|
|
160
196
|
|
|
161
197
|
Tapping an interaction bubble opens the Raylib-native **`modal_interact`** modal
|
|
162
|
-
(top half of the screen). It has a tab strip — **
|
|
163
|
-
|
|
164
|
-
|
|
198
|
+
(top half of the screen). It has a tab strip — **shop** (vendor catalog, shown
|
|
199
|
+
only when its action carries `shopItems`), **quest** (mission interface, shown
|
|
200
|
+
only when the entity provides quest codes), **stack** (active item slots), and
|
|
201
|
+
**stats** (six-stat stack totals) — over a fixed bottom bar of right-aligned
|
|
165
202
|
integration buttons (**Chat**, **Integration**) that open the JS overlay. The
|
|
166
|
-
|
|
203
|
+
paired `modal_dialogue` (bottom half) carries the talk flow.
|
|
204
|
+
|
|
205
|
+
Capability tabs lead the strip, and the leading one opens active — Shop for a
|
|
206
|
+
vendor, else Quest. Because the catalog resolves through an async REST fetch
|
|
207
|
+
after the modal is already open, the active tab keeps tracking the leading
|
|
208
|
+
capability until the player picks a tab themselves. Switching tabs plays a
|
|
209
|
+
pop-in transition, during which content taps are ignored.
|
|
167
210
|
|
|
168
211
|
The client is identical for `talk` and `quest-talk`; the **server** branches after
|
|
169
212
|
`dlg_complete`. The client never declares the action type, quest code, or quest
|
|
@@ -186,6 +229,25 @@ Binary uplink opcodes: `dlg_start` `0x17`, `dlg_complete` `0x18`, `dlg_cancel`
|
|
|
186
229
|
client upserts into its local `quest_store` (Quest Journal); it never gates
|
|
187
230
|
simulation state.
|
|
188
231
|
|
|
232
|
+
### Provider freeze
|
|
233
|
+
|
|
234
|
+
A dialogue is one step of a provider session, not the whole of it: the interact
|
|
235
|
+
modal stays open afterwards with its shop and quest tabs live. So when the
|
|
236
|
+
talked-to entity has a bound `CyberiaAction`, `dlg_complete` / `dlg_cancel`
|
|
237
|
+
re-bridge the freeze to `"interact"` instead of thawing, and `shop_buy` asserts
|
|
238
|
+
the same freeze before it mutates anything. The player therefore cannot be
|
|
239
|
+
killed anywhere inside a provider session, whether or not the client
|
|
240
|
+
re-asserted the freeze itself.
|
|
241
|
+
|
|
242
|
+
The client half holds up its end for as long as a modal is open. `modal_interact`,
|
|
243
|
+
`inventory_modal` and `modal_instance_map` each own a freeze reason
|
|
244
|
+
(`"interact"`, `"inventory"`, `"instance-map"`) and call
|
|
245
|
+
`local_player_keep_freeze()` every frame they stay open — without that renewal
|
|
246
|
+
the 30-second freeze watchdog auto-sends `freeze_end`, and a player browsing a
|
|
247
|
+
shop longer than that would silently become killable. A modal closing over
|
|
248
|
+
another one that still owns a freeze re-bridges to it rather than ending the
|
|
249
|
+
freeze, so there is never a thawed frame between them.
|
|
250
|
+
|
|
189
251
|
### Server `dlg_complete` handling
|
|
190
252
|
|
|
191
253
|
1. Validate `player.activeDialogueEntityID == msg.entityId`; drop otherwise.
|
|
@@ -292,16 +354,21 @@ The C client fetches the full `code` group sorted by `order`, then renders lines
|
|
|
292
354
|
|
|
293
355
|
```json
|
|
294
356
|
{
|
|
295
|
-
"code": "
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"
|
|
301
|
-
"
|
|
302
|
-
"
|
|
303
|
-
"shopItems": [],
|
|
357
|
+
"code": "loc-fallback-map-0-18-16",
|
|
358
|
+
"label": "Punk",
|
|
359
|
+
"sourceMapCode": "fallback-map-0",
|
|
360
|
+
"sourceCellX": 18,
|
|
361
|
+
"sourceCellY": 16,
|
|
362
|
+
"dialogCode": "default-punk",
|
|
363
|
+
"questDialogueCodes": [],
|
|
364
|
+
"shopItems": [{ "itemId": "tim-knife", "priceItemId": "coin", "priceQty": 10 }],
|
|
304
365
|
"craftRecipes": [],
|
|
305
366
|
"storageSlots": 0
|
|
306
367
|
}
|
|
307
368
|
```
|
|
369
|
+
|
|
370
|
+
This is the vendor shipped in the canonical defaults
|
|
371
|
+
(`DefaultCyberiaActions`): the `punk`-skinned NPC on `fallback-map-0` at
|
|
372
|
+
(18, 16) sells `tim-knife` for 10 coins. `bin/cyberia run-workflow
|
|
373
|
+
seed-actions-quests` upserts it; the procedural fallback world serves it
|
|
374
|
+
unpersisted.
|
|
@@ -133,6 +133,24 @@ Underpost Platform deploy orchestration ensures the backend layer is ready befor
|
|
|
133
133
|
|
|
134
134
|
---
|
|
135
135
|
|
|
136
|
+
## Edge tier
|
|
137
|
+
|
|
138
|
+
Every `cyberiaonline.com` hostname is served through one Envoy Gateway data plane over HTTP/1.1, HTTP/2 and HTTP/3 (QUIC), with TLS terminated per hostname by SNI. In development the certificates are self-signed and locally trusted, and the hostnames are mapped in `/etc/hosts`, so a browser reaches the real routing stack rather than a dev proxy.
|
|
139
|
+
|
|
140
|
+
Status pages never reach the engine at all, and the engine knows nothing about them. All three runtimes are agnostic: they return a standard HTTP status code or become unreachable. `underpost-gateway` — one shared Nginx workload in the gateway tier — proxies the site paths and intercepts those statuses, serving the declared document with the original URI and the original status code.
|
|
141
|
+
|
|
142
|
+
| Condition | Declared by | Answered from |
|
|
143
|
+
| --------------------------- | --------------------------------------- | -------------------------------------------------------- |
|
|
144
|
+
| `404` on any unmatched path | a `CyberiaPortal` view with path `/404` | `www.cyberiaonline.com/root/status-pages/404/index.html` |
|
|
145
|
+
| `502` / `503` / `504` | the view flagged `maintenanceDefault` | `www.cyberiaonline.com/root/maintenance/index.html` |
|
|
146
|
+
| `/offline`, `/maintenance` | `offlineDefault` / `maintenanceDefault` | the matching context directory |
|
|
147
|
+
|
|
148
|
+
A request to an unknown path reaches the portal, which answers a bare 404; the gateway swaps in the page. The address bar keeps the path the player typed, and the response is a true 404 — no redirect, no client-side script, and nothing for `engine-cyberia`, `cyberia-server` or `cyberia-client` to implement. API sub-paths bypass the interception, so a JSON 404 stays JSON.
|
|
149
|
+
|
|
150
|
+
Per-instance hosts follow the same layout under their own sub-path — `client.cyberiaonline.com/FOREST/status-pages/404/index.html` — from the `customStatusPages` entries in `conf.instances.json`. Both configuration files live in `engine-private/`, which is a private repository: expect the layout to be referenced without assuming the files are present locally.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
136
154
|
## Tick model
|
|
137
155
|
|
|
138
156
|
The tick is the universal coordinate of the simulation. Every server→client snapshot and every client→server input command carries a tick value.
|
|
@@ -176,14 +176,14 @@ cyberia chain unpause [--network besu-k8s]
|
|
|
176
176
|
|
|
177
177
|
Named scripts from the `scripts/` directory for seeding and build maintenance.
|
|
178
178
|
|
|
179
|
-
| Subcommand | Description
|
|
180
|
-
| ---------------------------- |
|
|
179
|
+
| Subcommand | Description |
|
|
180
|
+
| ---------------------------- | -------------------------------------------------------------------------------------- |
|
|
181
181
|
| `import-default-items` | Import default object layers, skills, dialogues, actions/quests, client-hints to Mongo |
|
|
182
|
-
| `seed-skills` | Upsert `DefaultSkillConfig` into the `cyberia-skill` collection (full records)
|
|
183
|
-
| `seed-dialogues` | Upsert `DefaultCyberiaDialogues` into the `cyberia-dialogue` collection
|
|
184
|
-
| `generate-semantic-examples` | Generate one procedural example per registered semantic prefix
|
|
185
|
-
| `build-manifest` | Build K8s Deployment + Service manifests for mmo-client / mmo-server
|
|
186
|
-
| `build-server-dashboard` | Build the static cyberia-server metrics/status dashboard (`--dev`, `--output-path`)
|
|
182
|
+
| `seed-skills` | Upsert `DefaultSkillConfig` into the `cyberia-skill` collection (full records) |
|
|
183
|
+
| `seed-dialogues` | Upsert `DefaultCyberiaDialogues` into the `cyberia-dialogue` collection |
|
|
184
|
+
| `generate-semantic-examples` | Generate one procedural example per registered semantic prefix |
|
|
185
|
+
| `build-manifest` | Build K8s Deployment + Service manifests for mmo-client / mmo-server |
|
|
186
|
+
| `build-server-dashboard` | Build the static cyberia-server metrics/status dashboard (`--dev`, `--output-path`) |
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
189
|
cyberia run-workflow import-default-items --env-path ./engine-private/conf/dd-cyberia/.env.development
|
|
@@ -195,6 +195,43 @@ cyberia run-workflow build-server-dashboard
|
|
|
195
195
|
|
|
196
196
|
---
|
|
197
197
|
|
|
198
|
+
## Bringing up the full stack locally
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
node bin run cluster 'express,dd-cyberia' --dev
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
One command, no extra flags. It resets and rebuilds the node, deploys MongoDB / IPFS / Valkey, imports each database from its git backup, installs the Gateway API control plane, and deploys `dd-cyberia` behind it.
|
|
205
|
+
|
|
206
|
+
What `--dev` implies, rather than requiring you to pass it:
|
|
207
|
+
|
|
208
|
+
- **Gateway API + Envoy Gateway**, with **HTTP/3 (QUIC) on by default** beside HTTP/2 and HTTP/1.1.
|
|
209
|
+
- **Self-signed, locally trusted TLS** for every hostname in `conf.server.json`, plus the matching `/etc/hosts` entries — so a local Chromium reaches `https://www.cyberiaonline.com` through the real data plane.
|
|
210
|
+
- **The gateway static tier seeded** with the portal's `/404`, `/offline` and `/maintenance` documents before the routes are applied, then refreshed from the running container once it is Ready. See [Architecture → Edge tier](./ARCHITECTURE.md).
|
|
211
|
+
|
|
212
|
+
The run ends with a gateway status report: listener and route conditions, the workloads behind them, and an HTTPS probe of every route hostname.
|
|
213
|
+
|
|
214
|
+
### With the MMO services
|
|
215
|
+
|
|
216
|
+
The optional third path segment brings up custom instances from `engine-private/conf/dd-cyberia/conf.instances.json` in the same run:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
node bin run cluster 'express,dd-cyberia,mmo-server' --dev
|
|
220
|
+
node bin run cluster 'express,dd-cyberia,mmo-server+mmo-client' --dev
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Each id runs only where `dd-cyberia` declares it, and only once the portal workload has rolled out — `cyberia-server` dials the engine's gRPC ClusterIP for its world configuration at boot, so the content authority has to be serving first. `mmo-server` names the whole variant family (`amethyst-strata-expansion`, `FOREST`, `TEST`); `mmo-server-forest` names one variant.
|
|
224
|
+
|
|
225
|
+
`server.cyberiaonline.com` and `client.cyberiaonline.com` are issued the same self-signed certificates as the portal hosts and written into the same `/etc/hosts` pass, so the three services are reachable over TLS from a local browser without further setup. In production the same segment issues cert-manager certificates instead.
|
|
226
|
+
|
|
227
|
+
To place the static documents again without redeploying — after rebuilding the portal client, for instance:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
node bin deploy dd-cyberia development --sync-static --gateway-api --kubeadm
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
198
235
|
## Operational rules
|
|
199
236
|
|
|
200
237
|
- Preserve public CLI entrypoints and command names unless a change is intentionally breaking.
|
|
@@ -23,12 +23,81 @@ import { shellExec } from '../server/process.js';
|
|
|
23
23
|
import { SitemapStream, streamToPromise } from 'sitemap';
|
|
24
24
|
import { Readable } from 'stream';
|
|
25
25
|
import { buildIcons } from './client-icons.js';
|
|
26
|
+
import { statusPageBuildSegment } from '../server/underpost-gateway.js';
|
|
26
27
|
import Underpost from '../index.js';
|
|
27
28
|
import { buildDocs } from './client-build-docs.js';
|
|
28
29
|
import { ssrFactory } from './ssr.js';
|
|
29
30
|
|
|
30
31
|
// Static Site Generation (SSG)
|
|
31
32
|
|
|
33
|
+
const STATUS_PAGE_VIEW_PATH = /^\/([1-5]\d{2})$/;
|
|
34
|
+
|
|
35
|
+
// Views a route intercepts before the workload sees the request. They are
|
|
36
|
+
// declared by the flag that already marks them as the app's default for that
|
|
37
|
+
// condition, so a new one becomes edge-served by adding its flag here rather
|
|
38
|
+
// than by naming its path in a second place.
|
|
39
|
+
const INTERCEPT_VIEW_FLAGS = ['maintenanceDefault', 'offlineDefault'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the SSR views that render an HTTP status page into the static
|
|
43
|
+
* artifacts they build to. A view is a status page when its route path is a
|
|
44
|
+
* bare status code (`/404`, `/500`, `/503`), which the build writes to
|
|
45
|
+
* `<path>/index.html` inside the served bundle.
|
|
46
|
+
*
|
|
47
|
+
* Single source of truth for PWA status-page routing: this build writes the
|
|
48
|
+
* artifact, and `deploy --build-manifest` points its HTTPRoute rules at the
|
|
49
|
+
* same resolved URL — neither side hardcodes a status code.
|
|
50
|
+
* @function statusPageRoutesFactory
|
|
51
|
+
* @param {Array<object>} [views] - SSR view entries from `conf.ssr.json`.
|
|
52
|
+
* @param {string} [proxyPath] - The client's proxy sub-path (`/`, `/peer`, ...).
|
|
53
|
+
* @returns {Array<{status: string, routePath: string, indexUrl: string, title: string, client: string}>}
|
|
54
|
+
* One entry per status view, in declaration order.
|
|
55
|
+
* @memberof clientBuild
|
|
56
|
+
*/
|
|
57
|
+
const statusPageRoutesFactory = ({ views = [], proxyPath = '/' } = {}) => {
|
|
58
|
+
const prefix = !proxyPath || proxyPath === '/' ? '' : proxyPath.replace(/\/$/, '');
|
|
59
|
+
return (Array.isArray(views) ? views : [])
|
|
60
|
+
.map((view) => ({ view, status: STATUS_PAGE_VIEW_PATH.exec(view?.path || '')?.[1] }))
|
|
61
|
+
.filter(({ status }) => status !== undefined)
|
|
62
|
+
.map(({ view, status }) => ({
|
|
63
|
+
status,
|
|
64
|
+
routePath: `${prefix}${view.path}`,
|
|
65
|
+
indexUrl: `${prefix}${view.path}/index.html`,
|
|
66
|
+
title: view.title,
|
|
67
|
+
client: view.client,
|
|
68
|
+
}));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolves the SSR views a gateway route intercepts and serves statically —
|
|
73
|
+
* the maintenance and offline documents. They carry no request-time logic, so
|
|
74
|
+
* the workload never needs to see them; the same build that writes the artifact
|
|
75
|
+
* hands `deploy --build-manifest` the URL its HTTPRoute rule targets.
|
|
76
|
+
* @function staticContextRoutesFactory
|
|
77
|
+
* @param {Array<object>} [views] - SSR view entries from `conf.ssr.json`.
|
|
78
|
+
* @param {string} [proxyPath] - The client's proxy sub-path (`/`, `/peer`, ...).
|
|
79
|
+
* @returns {Array<{context: string, routePath: string, indexUrl: string, title: string, client: string}>}
|
|
80
|
+
* One entry per intercepted view, in declaration order.
|
|
81
|
+
* @memberof clientBuild
|
|
82
|
+
*/
|
|
83
|
+
const staticContextRoutesFactory = ({ views = [], proxyPath = '/' } = {}) => {
|
|
84
|
+
const prefix = !proxyPath || proxyPath === '/' ? '' : proxyPath.replace(/\/$/, '');
|
|
85
|
+
return (Array.isArray(views) ? views : [])
|
|
86
|
+
.filter(
|
|
87
|
+
(view) =>
|
|
88
|
+
view?.path &&
|
|
89
|
+
!STATUS_PAGE_VIEW_PATH.test(view.path) &&
|
|
90
|
+
INTERCEPT_VIEW_FLAGS.some((flag) => view[flag] === true),
|
|
91
|
+
)
|
|
92
|
+
.map((view) => ({
|
|
93
|
+
context: view.path.replace(/^\/+|\/+$/g, ''),
|
|
94
|
+
routePath: `${prefix}${view.path}`,
|
|
95
|
+
indexUrl: `${prefix}${view.path}/index.html`,
|
|
96
|
+
title: view.title,
|
|
97
|
+
client: view.client,
|
|
98
|
+
}));
|
|
99
|
+
};
|
|
100
|
+
|
|
32
101
|
/**
|
|
33
102
|
* Recursively copies files from source to destination, but only files that don't exist in destination.
|
|
34
103
|
* @function copyNonExistingFiles
|
|
@@ -742,14 +811,12 @@ const buildClient = async (
|
|
|
742
811
|
}
|
|
743
812
|
|
|
744
813
|
if (views) {
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
(p) => p.srcBuildPath.startsWith(`./src/client/ssr`) || p.srcBuildPath.slice(-9) === '.index.js',
|
|
750
|
-
)
|
|
814
|
+
if (!(
|
|
815
|
+
enableLiveRebuild &&
|
|
816
|
+
!options.liveClientBuildPaths.find(
|
|
817
|
+
(p) => p.srcBuildPath.startsWith(`./src/client/ssr`) || p.srcBuildPath.slice(-9) === '.index.js',
|
|
751
818
|
)
|
|
752
|
-
)
|
|
819
|
+
))
|
|
753
820
|
for (const view of views) {
|
|
754
821
|
const buildPath = `${
|
|
755
822
|
rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
|
|
@@ -977,6 +1044,8 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
|
|
|
977
1044
|
// when the network is unreachable.
|
|
978
1045
|
const ssrClientConf = confSSR[getCapVariableName(client)] || {};
|
|
979
1046
|
const ssrViews = Array.isArray(ssrClientConf.views) ? ssrClientConf.views : [];
|
|
1047
|
+
const statusPageRoutes = statusPageRoutesFactory({ views: ssrViews, proxyPath: path });
|
|
1048
|
+
if (statusPageRoutes.length > 0) logger.info('ssr status page routes', statusPageRoutes);
|
|
980
1049
|
const PRE_CACHED_RESOURCES = [];
|
|
981
1050
|
let offlineFallbackUrl = null;
|
|
982
1051
|
let maintenanceFallbackUrl = null;
|
|
@@ -999,9 +1068,16 @@ Sitemap: ${sitemapBaseUrl}/sitemap.xml`,
|
|
|
999
1068
|
renderApi: { JSONweb },
|
|
1000
1069
|
});
|
|
1001
1070
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1071
|
+
// A status view is built under `status-pages/<status>/`, not on its own
|
|
1072
|
+
// `/<status>` route: the gateway serves it by intercepting the
|
|
1073
|
+
// runtime's error, and a document on that route would give the runtime
|
|
1074
|
+
// a page of its own to serve or redirect to for the same condition.
|
|
1075
|
+
const statusCode = statusPageRoutes.find((route) => route.routePath === `${proxyPrefix}${view.path}`)?.status;
|
|
1076
|
+
const clientRoot =
|
|
1077
|
+
rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath;
|
|
1078
|
+
const buildPath = statusCode
|
|
1079
|
+
? `${clientRoot}/${dir.dirname(statusPageBuildSegment(statusCode))}/`
|
|
1080
|
+
: `${clientRoot}${view.path === '/' ? view.path : `${view.path}/`}`;
|
|
1005
1081
|
|
|
1006
1082
|
const indexUrl = buildIndexUrl(view.path);
|
|
1007
1083
|
if (view.offlineDefault) {
|
|
@@ -1089,4 +1165,11 @@ ${swTransformedJs}`,
|
|
|
1089
1165
|
}
|
|
1090
1166
|
};
|
|
1091
1167
|
|
|
1092
|
-
export {
|
|
1168
|
+
export {
|
|
1169
|
+
buildClient,
|
|
1170
|
+
copyNonExistingFiles,
|
|
1171
|
+
unzipClientBuild,
|
|
1172
|
+
mergeClientBuildZip,
|
|
1173
|
+
staticContextRoutesFactory,
|
|
1174
|
+
statusPageRoutesFactory,
|
|
1175
|
+
};
|