@underpostnet/cyberia 3.2.80 → 3.3.0
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 +50 -15
- package/.github/workflows/coverall.cyberia.ci.yml +87 -0
- package/.github/workflows/cyberia-client.cd.yml +2 -7
- package/.github/workflows/cyberia-server.cd.yml +2 -7
- 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 +3 -67
- package/.github/workflows/ghpkg.ci.yml +7 -1
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -9
- package/CHANGELOG.md +291 -1
- package/CLI-HELP.md +174 -23
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +1 -1
- package/bin/build.js +7 -5
- package/bin/cyberia.js +272 -98
- package/bin/deploy.js +19 -17
- package/bin/index.js +272 -98
- package/compose.env +50 -15
- package/conf.js +2 -0
- package/deploy/dd-cyberia/deploy.sh +42 -0
- package/deploy/dd-cyberia/init.sh +63 -0
- package/deploy/dd-cyberia/sync-deploy.sh +194 -0
- package/deploy/lib/logging.sh +96 -0
- package/deploy/pwa-microservices-template/deploy.sh +72 -0
- package/deploy/release/deploy.sh +62 -0
- package/deployment.yaml +1 -210
- package/docker-compose.yml +90 -85
- package/hardhat/package-lock.json +139 -131
- package/hardhat/package.json +4 -4
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
- 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/audit-selinux.sh +64 -0
- package/scripts/coverall-test-cyberia.sh +24 -0
- package/scripts/coverall-test.sh +24 -0
- package/scripts/gpu-diag.sh +0 -0
- package/scripts/ip-info.sh +0 -0
- package/scripts/k3s-node-setup.sh +18 -15
- package/scripts/kubeadm-node-setup.sh +12 -23
- package/scripts/link-local-underpost-cli.sh +0 -0
- package/scripts/lxd-vm-setup.sh +0 -0
- package/scripts/maas-nat-firewalld.sh +0 -0
- package/scripts/nat-iptables.sh +12 -4
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- 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-default-items.js +63 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +49 -15
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +8 -12
- package/src/api/cyberia-instance/cyberia-instance.controller.js +3 -0
- package/src/api/cyberia-instance/cyberia-instance.router.js +13 -0
- package/src/api/cyberia-instance/cyberia-instance.service.js +43 -0
- 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-instance-conf/cyberia-instance-conf.model.js +1 -0
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +87 -1
- package/src/api/object-layer/object-layer.model.js +204 -3
- package/src/api/object-layer/object-layer.service.js +1 -14
- package/src/cli/baremetal.js +1 -2
- package/src/cli/cloud-init.js +1 -1
- package/src/cli/cluster.js +786 -96
- package/src/cli/db.js +11 -4
- package/src/cli/deploy.js +1698 -177
- package/src/cli/docker-compose.js +19 -178
- package/src/cli/env.js +1 -1
- package/src/cli/image.js +15 -7
- package/src/cli/index.js +245 -44
- package/src/cli/ipfs.js +82 -11
- package/src/cli/lxd.js +1 -1
- package/src/cli/monitor.js +2 -2
- package/src/cli/release.js +57 -22
- package/src/cli/repository.js +12 -10
- package/src/cli/run.js +2195 -427
- package/src/cli/secrets.js +969 -0
- package/src/cli/ssh.js +206 -105
- package/src/cli/system.js +26 -13
- package/src/cli/test.js +1 -1
- package/src/cli/vultr.js +583 -0
- package/src/cli/wireguard.js +2125 -0
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
- package/src/client/components/cyberia/InstanceSelectionView.js +11 -8
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +5 -0
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +33 -0
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +106 -39
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +35 -3
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +44 -7
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
- package/src/client/services/object-layer/object-layer.management.js +4 -4
- package/src/client-builder/client-build.js +102 -13
- package/src/client-builder/ssr.js +27 -73
- package/src/db/mongo/MongoBootstrap.js +295 -54
- package/src/db/mongo/MongooseDB.js +51 -32
- package/src/index.js +25 -1
- package/src/projects/cyberia/besu-genesis-generator.js +3 -2
- package/src/projects/cyberia/catalog-cyberia.js +5 -0
- package/src/projects/cyberia/generate-saga.js +14 -23
- package/src/projects/cyberia/hot-reload-trigger.js +3 -3
- package/src/projects/cyberia/instance-data.js +63 -3
- package/src/projects/cyberia/object-layer.js +11 -21
- package/src/projects/underpost/catalog-underpost.js +4 -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 +50 -15
- package/src/runtime/engine-cyberia/docker-compose.yml +90 -85
- package/src/runtime/engine-cyberia/nginx.conf +86 -16
- package/src/server/backup.js +1 -1
- package/src/server/conf.js +1216 -168
- package/src/server/cri.js +70 -0
- package/src/server/cron.js +249 -51
- package/src/server/dns.js +100 -6
- package/src/server/environment.js +98 -0
- package/src/server/forward-proxy.js +549 -0
- package/src/server/middlewares.js +56 -1
- package/src/server/process.js +0 -1
- package/src/server/selinux.js +185 -0
- package/src/server/systemd.js +205 -0
- package/src/server/underpost-compression.js +186 -0
- package/src/server/underpost-gateway.js +1083 -0
- package/src/server/underpost-ingress.js +380 -0
- package/test/cluster-instances.test.js +435 -0
- package/test/cyberia-instance-conf-defaults.test.js +1 -0
- package/test/deploy-node-placement.test.js +45 -0
- package/test/instance-traffic-plan.test.js +710 -0
- package/test/selinux.test.js +71 -0
- package/test/sops-secret-store.test.js +612 -0
- package/test/underpost-gateway.test.js +510 -0
- package/test/underpost-ingress.test.js +305 -0
- package/test/wireguard-edge.test.js +1177 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swappable random source for procedural world generation.
|
|
3
|
+
*
|
|
4
|
+
* The world generator and portal connector draw all randomness through
|
|
5
|
+
* `nextRandom()` instead of `Math.random()` directly. By default it IS
|
|
6
|
+
* `Math.random`, so live/editor generation stays random. The fallback world
|
|
7
|
+
* temporarily installs a seeded PRNG so the SAME layout is produced on every
|
|
8
|
+
* call — the instance-map `/static` POIs and the `/preview` image are built by
|
|
9
|
+
* independent HTTP requests (and survive server restarts), so they must agree
|
|
10
|
+
* on where every entity sits.
|
|
11
|
+
*
|
|
12
|
+
* @module src/api/cyberia-instance/cyberia-random-source.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
let source = Math.random;
|
|
16
|
+
|
|
17
|
+
/** Draw the next float in [0, 1). Routes through the installed source. */
|
|
18
|
+
const nextRandom = () => source();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* mulberry32 — a small, fast, well-distributed seeded PRNG. Deterministic:
|
|
22
|
+
* the same seed always yields the same sequence.
|
|
23
|
+
* @param {number} seed 32-bit unsigned integer
|
|
24
|
+
* @returns {() => number}
|
|
25
|
+
*/
|
|
26
|
+
function mulberry32(seed) {
|
|
27
|
+
let a = seed >>> 0;
|
|
28
|
+
return function () {
|
|
29
|
+
a |= 0;
|
|
30
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
31
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
32
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
33
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Derive a stable 32-bit seed from a string (FNV-1a). */
|
|
38
|
+
function hashSeed(str) {
|
|
39
|
+
let h = 0x811c9dc5;
|
|
40
|
+
for (let i = 0; i < String(str).length; i++) {
|
|
41
|
+
h ^= String(str).charCodeAt(i);
|
|
42
|
+
h = Math.imul(h, 0x01000193);
|
|
43
|
+
}
|
|
44
|
+
return h >>> 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Install a deterministic seeded source. Accepts a number or a string
|
|
49
|
+
* (hashed to a 32-bit seed). Call `resetRandomSource()` to restore Math.random.
|
|
50
|
+
* @param {number|string} seed
|
|
51
|
+
*/
|
|
52
|
+
function seedRandomSource(seed) {
|
|
53
|
+
const s = typeof seed === 'number' ? seed >>> 0 : hashSeed(seed);
|
|
54
|
+
source = mulberry32(s);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Restore the default `Math.random` source. */
|
|
58
|
+
function resetRandomSource() {
|
|
59
|
+
source = Math.random;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run `fn` with a deterministic seeded source, restoring the previous source
|
|
64
|
+
* afterward even if `fn` throws. Returns `fn`'s result.
|
|
65
|
+
* @template T
|
|
66
|
+
* @param {number|string} seed
|
|
67
|
+
* @param {() => T} fn
|
|
68
|
+
* @returns {T}
|
|
69
|
+
*/
|
|
70
|
+
function withSeededRandom(seed, fn) {
|
|
71
|
+
const prev = source;
|
|
72
|
+
seedRandomSource(seed);
|
|
73
|
+
try {
|
|
74
|
+
return fn();
|
|
75
|
+
} finally {
|
|
76
|
+
source = prev;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { nextRandom, seedRandomSource, resetRandomSource, withSeededRandom, hashSeed };
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from '../cyberia-server-defaults/cyberia-server-defaults.js';
|
|
24
24
|
|
|
25
25
|
import { DefaultCyberiaItems, ITEM_TYPES } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
|
|
26
|
+
import { nextRandom } from './cyberia-random-source.js';
|
|
26
27
|
|
|
27
28
|
import { PORTAL_MODES, PORTAL_MODE_COLOR_KEY, EXTRA_PORTAL_MODES } from './cyberia-portal-connector.js';
|
|
28
29
|
|
|
@@ -51,7 +52,7 @@ const findColor = (colors, key) => colors.find((c) => c.key === key);
|
|
|
51
52
|
* @param {number} max
|
|
52
53
|
* @returns {number}
|
|
53
54
|
*/
|
|
54
|
-
const randInt = (min, max) => min + Math.floor(
|
|
55
|
+
const randInt = (min, max) => min + Math.floor(nextRandom() * (max - min + 1));
|
|
55
56
|
|
|
56
57
|
// ── Entity count ranges ──────────────────────────────────────────────────────
|
|
57
58
|
// [min, max] — actual count is random within range on each generation call.
|
|
@@ -438,7 +439,7 @@ function generatePortalEntities(mapDims, colors, opts = {}) {
|
|
|
438
439
|
// First portal is always inter-portal (reserved for the ring topology);
|
|
439
440
|
// extra portals get a random non-ring subtype.
|
|
440
441
|
const subtype =
|
|
441
|
-
i === 0 ? PORTAL_MODES.INTER_PORTAL : EXTRA_PORTAL_MODES[Math.floor(
|
|
442
|
+
i === 0 ? PORTAL_MODES.INTER_PORTAL : EXTRA_PORTAL_MODES[Math.floor(nextRandom() * EXTRA_PORTAL_MODES.length)];
|
|
442
443
|
const portal = generatePortalEntity(mapDims, colors, opts.grid, subtype);
|
|
443
444
|
if (portal) entities.push(portal);
|
|
444
445
|
}
|
|
@@ -484,7 +485,7 @@ function generateBots(mapDims, colors, opts = {}) {
|
|
|
484
485
|
|
|
485
486
|
// const skin = BOT_SKIN_POOL[Math.floor(Math.random() * BOT_SKIN_POOL.length)];
|
|
486
487
|
const skin = 'kishins';
|
|
487
|
-
const hasWeapon =
|
|
488
|
+
const hasWeapon = nextRandom() < BOT_WEAPON_CHANCE;
|
|
488
489
|
const itemIds = hasWeapon ? [skin, 'atlas_pistol_mk2'] : [skin];
|
|
489
490
|
|
|
490
491
|
entities.push({
|
|
@@ -148,6 +148,7 @@ const CyberiaInstanceConfSchema = new Schema(
|
|
|
148
148
|
// ── Player defaults ──────────────────────────────────────────────
|
|
149
149
|
defaultPlayerWidth: { type: Number, default: D.defaultPlayerWidth },
|
|
150
150
|
defaultPlayerHeight: { type: Number, default: D.defaultPlayerHeight },
|
|
151
|
+
playerBaseSpeed: { type: Number, default: D.playerBaseSpeed },
|
|
151
152
|
playerBaseLifeRegenMin: { type: Number, default: D.playerBaseLifeRegenMin },
|
|
152
153
|
playerBaseLifeRegenMax: { type: Number, default: D.playerBaseLifeRegenMax },
|
|
153
154
|
sumStatsLimit: { type: Number, default: D.sumStatsLimit },
|
|
@@ -591,6 +591,76 @@ export const DefaultCyberiaActions = [
|
|
|
591
591
|
dialogCode: 'default-agent',
|
|
592
592
|
questDialogueCodes: [{ questCode: 'bounty-quest-alpha', dialogCode: 'quest-talk-agent' }],
|
|
593
593
|
},
|
|
594
|
+
{
|
|
595
|
+
// Shop capability: an action carrying shopItems is a vendor. The client
|
|
596
|
+
// surfaces a Shop tab for it and the simulation validates every purchase
|
|
597
|
+
// against this catalog (price item + quantity).
|
|
598
|
+
code: 'loc-fallback-map-0-18-16',
|
|
599
|
+
label: 'Punk',
|
|
600
|
+
sourceMapCode: 'fallback-map-0',
|
|
601
|
+
sourceCellX: 18,
|
|
602
|
+
sourceCellY: 16,
|
|
603
|
+
dialogCode: 'default-punk',
|
|
604
|
+
shopItems: [{ itemId: 'tim-knife', priceItemId: 'coin', priceQty: 10 }],
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
// Assembler capability: an action carrying craftRecipes is a fabrication
|
|
608
|
+
// terminal. The client surfaces an Assembly tab for it and the simulation
|
|
609
|
+
// validates every synthesis against these recipes (ingredients → outputs).
|
|
610
|
+
// Every id here is obtainable inside the fallback world — wood from the
|
|
611
|
+
// resource nodes, coin from field drops — so the loop is playable on a
|
|
612
|
+
// fresh spawn with no seeding.
|
|
613
|
+
code: 'loc-fallback-map-0-15-16',
|
|
614
|
+
label: 'Eiri',
|
|
615
|
+
sourceMapCode: 'fallback-map-0',
|
|
616
|
+
sourceCellX: 15,
|
|
617
|
+
sourceCellY: 16,
|
|
618
|
+
dialogCode: 'default-eiri',
|
|
619
|
+
craftRecipes: [
|
|
620
|
+
{
|
|
621
|
+
outputItems: [{ itemId: 'hatchet', qty: 1 }],
|
|
622
|
+
ingredients: [
|
|
623
|
+
{ itemId: 'wood-drop-1', qty: 2 },
|
|
624
|
+
{ itemId: 'coin', qty: 5 },
|
|
625
|
+
],
|
|
626
|
+
craftTimeMs: 3000,
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
outputItems: [{ itemId: 'tim-knife', qty: 1 }],
|
|
630
|
+
ingredients: [
|
|
631
|
+
{ itemId: 'wood-drop-2', qty: 1 },
|
|
632
|
+
{ itemId: 'wood-drop-1', qty: 1 },
|
|
633
|
+
{ itemId: 'coin', qty: 10 },
|
|
634
|
+
],
|
|
635
|
+
craftTimeMs: 5000,
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
// Multi-output: one blade breaks down into three component stacks, so
|
|
639
|
+
// the assembly UI and the arrival flights are exercised with more than
|
|
640
|
+
// a single result slot. Closes the loop with Punk's shop — buy a knife,
|
|
641
|
+
// salvage it back into parts.
|
|
642
|
+
outputItems: [
|
|
643
|
+
{ itemId: 'wood-drop-1', qty: 2 },
|
|
644
|
+
{ itemId: 'wood-drop-2', qty: 1 },
|
|
645
|
+
{ itemId: 'coin', qty: 5 },
|
|
646
|
+
],
|
|
647
|
+
ingredients: [{ itemId: 'tim-knife', qty: 1 }],
|
|
648
|
+
craftTimeMs: 2000,
|
|
649
|
+
},
|
|
650
|
+
],
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
// Storage capability: an action carrying storageSlots is a personal vault.
|
|
654
|
+
// The client surfaces a Storage tab whose square grid is sized from the
|
|
655
|
+
// capacity (25 → 5x5) and the simulation owns the contents.
|
|
656
|
+
code: 'loc-fallback-map-0-12-22',
|
|
657
|
+
label: 'Kaneki',
|
|
658
|
+
sourceMapCode: 'fallback-map-0',
|
|
659
|
+
sourceCellX: 12,
|
|
660
|
+
sourceCellY: 22,
|
|
661
|
+
dialogCode: 'default-kaneki',
|
|
662
|
+
storageSlots: 25,
|
|
663
|
+
},
|
|
594
664
|
{
|
|
595
665
|
code: 'loc-fallback-map-0-15-22',
|
|
596
666
|
label: 'Lain',
|
|
@@ -839,7 +909,6 @@ export const ENTITY_TYPE_DEFAULTS = Object.freeze([
|
|
|
839
909
|
{ itemId: 'anon', active: true, quantity: 1 },
|
|
840
910
|
{ itemId: 'atlas_pistol_mk2', active: true, quantity: 1 },
|
|
841
911
|
{ itemId: 'coin', active: false, quantity: 0 },
|
|
842
|
-
{ itemId: 'eiri', active: false, quantity: 1 },
|
|
843
912
|
],
|
|
844
913
|
},
|
|
845
914
|
{
|
|
@@ -868,6 +937,19 @@ export const ENTITY_TYPE_DEFAULTS = Object.freeze([
|
|
|
868
937
|
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['wason'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
|
|
869
938
|
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['alex'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
|
|
870
939
|
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['agent'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
|
|
940
|
+
{ entityType: ENTITY_TYPES.bot, liveItemIds: ['punk'], deadItemIds: [DEFAULT_DEAD_ITEM_ID], behavior: 'provider' },
|
|
941
|
+
{
|
|
942
|
+
entityType: ENTITY_TYPES.bot,
|
|
943
|
+
liveItemIds: ['eiri'],
|
|
944
|
+
deadItemIds: [DEFAULT_DEAD_ITEM_ID],
|
|
945
|
+
behavior: 'provider-static',
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
entityType: ENTITY_TYPES.bot,
|
|
949
|
+
liveItemIds: ['kaneki'],
|
|
950
|
+
deadItemIds: [DEFAULT_DEAD_ITEM_ID],
|
|
951
|
+
behavior: 'provider-static',
|
|
952
|
+
},
|
|
871
953
|
{
|
|
872
954
|
entityType: ENTITY_TYPES.bot,
|
|
873
955
|
liveItemIds: ['lain'],
|
|
@@ -954,6 +1036,10 @@ export const CYBERIA_INSTANCE_CONF_DEFAULTS = {
|
|
|
954
1036
|
defaultPlayerHeight: 2,
|
|
955
1037
|
playerBaseLifeRegenMin: 0.5,
|
|
956
1038
|
playerBaseLifeRegenMax: 1.5,
|
|
1039
|
+
// Movement speed for the player entity only, in grid cells per second. Bots,
|
|
1040
|
+
// projectiles and every other entity keep entityBaseSpeed. 0 falls back to
|
|
1041
|
+
// entityBaseSpeed.
|
|
1042
|
+
playerBaseSpeed: 8,
|
|
957
1043
|
sumStatsLimit: 500,
|
|
958
1044
|
maxActiveLayers: 4,
|
|
959
1045
|
initialLifeFraction: 1.0,
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @module src/api/object-layer/object-layer.model.js
|
|
4
4
|
* @namespace CyberiaObjectLayerModel
|
|
5
5
|
*/
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import stringify from 'fast-json-stable-stringify';
|
|
6
8
|
import { Schema, model } from 'mongoose';
|
|
7
9
|
/**
|
|
8
10
|
* @typedef {Object} Stats
|
|
@@ -132,8 +134,19 @@ const ObjectLayerSchema = new Schema(
|
|
|
132
134
|
toObject: { virtuals: true },
|
|
133
135
|
},
|
|
134
136
|
);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Name of the `data.item.id` index. Kept stable across the non-unique → unique
|
|
139
|
+
* upgrade so {@link ObjectLayerModel.ensureUniqueItemIdIndex} can detect and
|
|
140
|
+
* replace the legacy definition instead of leaving two overlapping indexes.
|
|
141
|
+
* @memberof CyberiaObjectLayerModel
|
|
142
|
+
*/
|
|
143
|
+
const ITEM_ID_INDEX_NAME = 'data.item.id_1';
|
|
144
|
+
// `data.item.id` is the natural key: exactly one document per item id.
|
|
145
|
+
// autoIndex runs at model-compile time, so on a collection that still holds
|
|
146
|
+
// legacy duplicates (or a legacy non-unique index) this build fails and is
|
|
147
|
+
// logged by the model's `error` listener. ensureUniqueItemIdIndex() then
|
|
148
|
+
// dedupes and rebuilds it, after which autoIndex agrees and stays quiet.
|
|
149
|
+
ObjectLayerSchema.index({ 'data.item.id': 1 }, { name: ITEM_ID_INDEX_NAME, unique: true });
|
|
137
150
|
ObjectLayerSchema.index({ 'data.item.type': 1 });
|
|
138
151
|
// Add text index for searchable fields
|
|
139
152
|
ObjectLayerSchema.index(
|
|
@@ -158,6 +171,186 @@ ObjectLayerSchema.pre('save', function () {
|
|
|
158
171
|
}
|
|
159
172
|
// cid (object layer data JSON) and data.render.cid (atlas PNG) are optional – default to ''
|
|
160
173
|
});
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Computes the canonical SHA-256 of an object layer `data` sub-document using
|
|
177
|
+
* deterministic JSON serialisation. Single source of truth for the hash;
|
|
178
|
+
* `ObjectLayerEngine.computeSha256` delegates here.
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} data - The `data` sub-document (item, stats, ledger, render).
|
|
181
|
+
* @returns {string} Hex-encoded SHA-256 hash.
|
|
182
|
+
* @memberof CyberiaObjectLayerModel
|
|
183
|
+
*/
|
|
184
|
+
const computeObjectLayerSha256 = (data) => crypto.createHash('sha256').update(stringify(data)).digest('hex');
|
|
185
|
+
|
|
186
|
+
const isMergeableObject = (value) => {
|
|
187
|
+
if (value === null || typeof value !== 'object') return false;
|
|
188
|
+
const proto = Object.getPrototypeOf(value);
|
|
189
|
+
return proto === Object.prototype || proto === null;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* An incoming attribute only wins when it actually carries a value. `null`,
|
|
194
|
+
* `undefined` and `''` mean "not provided by this writer" — importers emit them
|
|
195
|
+
* whenever a stage is skipped or degraded (IPFS unreachable, atlas not
|
|
196
|
+
* generated, saga metadata with no render yet), and they must never erase a
|
|
197
|
+
* value another writer already persisted. `false` and `0` are real values.
|
|
198
|
+
*
|
|
199
|
+
* @param {*} value - Candidate attribute value.
|
|
200
|
+
* @returns {boolean} Whether the value should override the stored one.
|
|
201
|
+
* @memberof CyberiaObjectLayerModel
|
|
202
|
+
*/
|
|
203
|
+
const hasValue = (value) => value !== null && value !== undefined && value !== '';
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Deep-merges `incoming` over `existing`, keeping the last attribute that
|
|
207
|
+
* actually carries a value. Plain objects merge key by key; every other value
|
|
208
|
+
* (scalars, arrays, ObjectIds, Dates, Buffers) is replaced atomically.
|
|
209
|
+
*
|
|
210
|
+
* @param {*} existing - Currently persisted value.
|
|
211
|
+
* @param {*} incoming - Value received from the writer.
|
|
212
|
+
* @returns {*} The merged value.
|
|
213
|
+
* @memberof CyberiaObjectLayerModel
|
|
214
|
+
*/
|
|
215
|
+
function mergeObjectLayerData(existing, incoming) {
|
|
216
|
+
if (isMergeableObject(incoming)) {
|
|
217
|
+
if (!isMergeableObject(existing)) return incoming;
|
|
218
|
+
const merged = { ...existing };
|
|
219
|
+
for (const key of Object.keys(incoming)) merged[key] = mergeObjectLayerData(existing[key], incoming[key]);
|
|
220
|
+
return merged;
|
|
221
|
+
}
|
|
222
|
+
return hasValue(incoming) ? incoming : existing;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Document fields an upsert may carry. `sha256` is excluded on purpose: it is
|
|
227
|
+
* always recomputed from the merged `data`, so it can never drift from the
|
|
228
|
+
* payload the writer hashed before merging.
|
|
229
|
+
* @memberof CyberiaObjectLayerModel
|
|
230
|
+
*/
|
|
231
|
+
const UPSERTABLE_FIELDS = ['data', 'cid', 'objectLayerRenderFramesId', 'atlasSpriteSheetId'];
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Collapses any pre-existing duplicates of a given `data.item.id` down to a
|
|
235
|
+
* single document, keeping the oldest one so `_id` references held elsewhere
|
|
236
|
+
* (instances, inventories, atlas metadata) stay valid.
|
|
237
|
+
*
|
|
238
|
+
* @param {import('mongoose').Model} Model - The bound ObjectLayer model.
|
|
239
|
+
* @param {string} itemId - The item id to collapse.
|
|
240
|
+
* @returns {Promise<{ survivor: Object|null, removedIds: Array }>} Survivor and removed document ids.
|
|
241
|
+
* @memberof CyberiaObjectLayerModel
|
|
242
|
+
*/
|
|
243
|
+
async function collapseItemIdDuplicates(Model, itemId) {
|
|
244
|
+
const [survivor, ...duplicates] = await Model.find({ 'data.item.id': itemId }).sort({ createdAt: 1, _id: 1 });
|
|
245
|
+
if (!survivor || duplicates.length === 0) return { survivor: survivor || null, removedIds: [] };
|
|
246
|
+
|
|
247
|
+
const removedIds = duplicates.map((duplicate) => duplicate._id);
|
|
248
|
+
await Model.deleteMany({ _id: { $in: removedIds } });
|
|
249
|
+
|
|
250
|
+
// Orphaned render frames have no other cleanup path once their owner is gone.
|
|
251
|
+
const renderFramesIds = duplicates.map((duplicate) => duplicate.objectLayerRenderFramesId).filter(Boolean);
|
|
252
|
+
if (renderFramesIds.length > 0 && Model.db.models.ObjectLayerRenderFrames) {
|
|
253
|
+
await Model.db.models.ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFramesIds } });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { survivor, removedIds };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolves the canonical document for an item id. While a legacy collection can
|
|
261
|
+
* still hold duplicates, an unsorted `findOne` may hand back a document that the
|
|
262
|
+
* next upsert collapses away, so readers and writers must agree on the same
|
|
263
|
+
* survivor: the oldest one.
|
|
264
|
+
*
|
|
265
|
+
* @param {string} itemId - The `data.item.id` to look up.
|
|
266
|
+
* @returns {import('mongoose').Query} Query resolving to the canonical document or `null`.
|
|
267
|
+
* @memberof CyberiaObjectLayerModel
|
|
268
|
+
*/
|
|
269
|
+
ObjectLayerSchema.statics.findByItemId = function (itemId) {
|
|
270
|
+
return this.findOne({ 'data.item.id': itemId }).sort({ createdAt: 1, _id: 1 });
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Dedupes the whole collection by `data.item.id`.
|
|
275
|
+
*
|
|
276
|
+
* @returns {Promise<Array>} Ids of the removed duplicate documents.
|
|
277
|
+
* @memberof CyberiaObjectLayerModel
|
|
278
|
+
*/
|
|
279
|
+
ObjectLayerSchema.statics.dedupeByItemId = async function () {
|
|
280
|
+
const duplicated = await this.aggregate([
|
|
281
|
+
{ $group: { _id: '$data.item.id', count: { $sum: 1 } } },
|
|
282
|
+
{ $match: { count: { $gt: 1 } } },
|
|
283
|
+
]);
|
|
284
|
+
const removedIds = [];
|
|
285
|
+
for (const { _id: itemId } of duplicated) {
|
|
286
|
+
if (itemId === null || itemId === undefined) continue;
|
|
287
|
+
const { removedIds: removed } = await collapseItemIdDuplicates(this, itemId);
|
|
288
|
+
removedIds.push(...removed);
|
|
289
|
+
}
|
|
290
|
+
return removedIds;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Idempotent migration that enforces the one-document-per-`data.item.id`
|
|
295
|
+
* invariant at the storage layer: dedupes first, then upgrades the legacy
|
|
296
|
+
* non-unique index to a unique one. Safe to rerun; a no-op once applied.
|
|
297
|
+
*
|
|
298
|
+
* @returns {Promise<{ removedIds: Array, indexUpgraded: boolean }>} Migration outcome.
|
|
299
|
+
* @memberof CyberiaObjectLayerModel
|
|
300
|
+
*/
|
|
301
|
+
ObjectLayerSchema.statics.ensureUniqueItemIdIndex = async function () {
|
|
302
|
+
const removedIds = await this.dedupeByItemId();
|
|
303
|
+
|
|
304
|
+
const indexes = await this.collection.indexes().catch(() => []);
|
|
305
|
+
const current = indexes.find((index) => index.name === ITEM_ID_INDEX_NAME);
|
|
306
|
+
if (current?.unique) return { removedIds, indexUpgraded: false };
|
|
307
|
+
|
|
308
|
+
if (current) await this.collection.dropIndex(ITEM_ID_INDEX_NAME);
|
|
309
|
+
await this.collection.createIndex({ 'data.item.id': 1 }, { name: ITEM_ID_INDEX_NAME, unique: true });
|
|
310
|
+
return { removedIds, indexUpgraded: true };
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The single write path for object layers keyed by item id.
|
|
315
|
+
*
|
|
316
|
+
* Guarantees that exactly one document exists per `data.item.id`: pre-existing
|
|
317
|
+
* duplicates are collapsed onto the oldest document, which is then updated in
|
|
318
|
+
* place. Attributes are merged with {@link mergeObjectLayerData}, so a writer
|
|
319
|
+
* that omits a value (or sends `null` / `''` because a stage was skipped) keeps
|
|
320
|
+
* whatever a previous writer stored instead of erasing it. `sha256` is always
|
|
321
|
+
* recomputed from the merged result.
|
|
322
|
+
*
|
|
323
|
+
* @param {Object} payload - Document payload; `data.item.id` is required.
|
|
324
|
+
* @param {Object} payload.data - Object layer data (item, stats, ledger, render).
|
|
325
|
+
* @param {Object} [options] - Upsert options.
|
|
326
|
+
* @param {Object} [options.setOnInsert=null] - Partial document applied only when the item is new,
|
|
327
|
+
* for fields a writer wants to seed but never refresh (mirrors Mongo's `$setOnInsert`).
|
|
328
|
+
* @returns {Promise<Object>} The single surviving ObjectLayer document.
|
|
329
|
+
* @memberof CyberiaObjectLayerModel
|
|
330
|
+
*/
|
|
331
|
+
ObjectLayerSchema.statics.upsertByItemId = async function (payload, { setOnInsert = null } = {}) {
|
|
332
|
+
const itemId = payload?.data?.item?.id;
|
|
333
|
+
if (!itemId) throw new Error('ObjectLayer.upsertByItemId requires data.item.id');
|
|
334
|
+
|
|
335
|
+
const { survivor } = await collapseItemIdDuplicates(this, itemId);
|
|
336
|
+
|
|
337
|
+
if (!survivor) {
|
|
338
|
+
const inserted = setOnInsert ? mergeObjectLayerData(setOnInsert, payload) : payload;
|
|
339
|
+
return await this.create({ ...inserted, sha256: computeObjectLayerSha256(inserted.data) });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const existing = survivor.toObject({ virtuals: false, depopulate: true });
|
|
343
|
+
const update = {};
|
|
344
|
+
for (const field of UPSERTABLE_FIELDS) {
|
|
345
|
+
if (!(field in payload)) continue;
|
|
346
|
+
const merged = mergeObjectLayerData(existing[field], payload[field]);
|
|
347
|
+
if (merged !== undefined) update[field] = merged;
|
|
348
|
+
}
|
|
349
|
+
update.sha256 = computeObjectLayerSha256(update.data ?? existing.data);
|
|
350
|
+
|
|
351
|
+
return await this.findByIdAndUpdate(survivor._id, { $set: update }, { returnDocument: 'after' });
|
|
352
|
+
};
|
|
353
|
+
|
|
161
354
|
// Create and export the model
|
|
162
355
|
const ObjectLayerModel = model('ObjectLayer', ObjectLayerSchema);
|
|
163
356
|
const ProviderSchema = ObjectLayerSchema;
|
|
@@ -197,4 +390,12 @@ class ObjectLayerDto {
|
|
|
197
390
|
},
|
|
198
391
|
};
|
|
199
392
|
}
|
|
200
|
-
export {
|
|
393
|
+
export {
|
|
394
|
+
ObjectLayerSchema,
|
|
395
|
+
ObjectLayerModel,
|
|
396
|
+
ProviderSchema,
|
|
397
|
+
ObjectLayerDto,
|
|
398
|
+
computeObjectLayerSha256,
|
|
399
|
+
mergeObjectLayerData,
|
|
400
|
+
ITEM_ID_INDEX_NAME,
|
|
401
|
+
};
|
|
@@ -215,9 +215,6 @@ class ObjectLayerService {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
// Compute final SHA-256 with all CIDs
|
|
219
|
-
bodyData.sha256 = ObjectLayerEngine.computeSha256(bodyData.data);
|
|
220
|
-
|
|
221
218
|
// Pin data JSON to IPFS
|
|
222
219
|
try {
|
|
223
220
|
const itemId = bodyData.data.item.id;
|
|
@@ -232,17 +229,7 @@ class ObjectLayerService {
|
|
|
232
229
|
}
|
|
233
230
|
|
|
234
231
|
// Atomic create/replace – ObjectLayer is fully populated with all CIDs
|
|
235
|
-
|
|
236
|
-
const existingByItemId = await ObjectLayer.findOne({ 'data.item.id': bodyData.data.item.id });
|
|
237
|
-
if (existingByItemId) {
|
|
238
|
-
newObjectLayer = await ObjectLayer.findByIdAndUpdate(existingByItemId._id, bodyData, {
|
|
239
|
-
returnDocument: 'after',
|
|
240
|
-
}).populate('objectLayerRenderFramesId');
|
|
241
|
-
} else {
|
|
242
|
-
newObjectLayer = await (await new ObjectLayer(bodyData).save()).populate('objectLayerRenderFramesId');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return newObjectLayer;
|
|
232
|
+
return await (await ObjectLayer.upsertByItemId(bodyData)).populate('objectLayerRenderFramesId');
|
|
246
233
|
};
|
|
247
234
|
|
|
248
235
|
/**
|
package/src/cli/baremetal.js
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
-
import { getNpmRootPath
|
|
8
|
+
import { getNpmRootPath } from '../server/environment.js';
|
|
9
9
|
import { pbcopy, shellExec } from '../server/process.js';
|
|
10
|
-
import dotenv from 'dotenv';
|
|
11
10
|
import { loggerFactory, loggerMiddleware } from '../server/logger.js';
|
|
12
11
|
import fs from 'fs-extra';
|
|
13
12
|
import path from 'path';
|
package/src/cli/cloud-init.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { shellExec } from '../server/process.js';
|
|
9
9
|
import fs from 'fs-extra';
|
|
10
10
|
import { loggerFactory } from '../server/logger.js';
|
|
11
|
-
import { getNpmRootPath } from '../server/
|
|
11
|
+
import { getNpmRootPath } from '../server/environment.js';
|
|
12
12
|
import Underpost from '../index.js';
|
|
13
13
|
|
|
14
14
|
const logger = loggerFactory(import.meta);
|