@underpostnet/cyberia 3.3.73 → 3.3.77
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 +17 -5
- package/.github/workflows/coverall.cyberia.ci.yml +10 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/engine-cyberia.ci.yml +1 -1
- package/.github/workflows/publish.cyberia.ci.yml +44 -0
- package/AGENTS.md +61 -12
- package/CHANGELOG.md +159 -161
- package/CLI-HELP.md +20 -13
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +2 -2
- package/README.md +28 -28
- package/bin/build.js +96 -21
- package/bin/cyberia.js +1247 -1373
- package/bin/index.js +1247 -1373
- package/compose.env +17 -5
- package/conf.js +7 -4
- package/deploy/cyberia-client/deploy.sh +10 -11
- package/deploy/cyberia-client/package.sh +0 -1
- package/deploy/cyberia-client/state.sh +3 -2
- package/deploy/cyberia-server/deploy.sh +10 -11
- package/deploy/cyberia-server/package.sh +0 -1
- package/deploy/cyberia-server/state.sh +3 -2
- package/deploy/dd-cyberia/deploy.sh +5 -5
- package/deploy/dd-cyberia/init.sh +15 -14
- package/deploy/dd-cyberia/package.sh +0 -1
- package/deploy/dd-cyberia/state.sh +2 -2
- package/deploy/dd-cyberia/sync-deploy.sh +139 -75
- package/deploy/lib/config.sh +29 -0
- package/deploy/lib/github-actions-logging.sh +92 -76
- package/deploy/lib/host.sh +28 -19
- package/deploy/lib/state.sh +4 -1
- package/deploy/pwa-microservices-template/deploy.sh +10 -8
- package/deploy/release/deploy.sh +0 -2
- package/deployment.yaml +1 -1
- package/docker-compose.yml +23 -17
- package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
- package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
- package/docs/coverage/cyberia/base.css +362 -0
- package/docs/coverage/cyberia/block-navigation.js +82 -0
- package/docs/coverage/cyberia/favicon.png +0 -0
- package/docs/coverage/cyberia/index.html +161 -0
- package/docs/coverage/cyberia/prettify.css +101 -0
- package/docs/coverage/cyberia/prettify.js +937 -0
- package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
- package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
- package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
- package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
- package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
- package/docs/coverage/cyberia/sorter.js +205 -0
- package/hardhat/package-lock.json +2 -2
- package/hardhat/package.json +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/mongo-express/deployment.yaml +46 -27
- package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
- package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
- package/manifests/deployment/mongo-express/service.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
- package/package.json +19 -17
- package/scripts/test-monitor.sh +3 -3
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
- package/src/api/cyberia-action/cyberia-action.router.js +2 -2
- package/src/api/cyberia-action/cyberia-action.service.js +3 -4
- package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
- package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
- package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
- package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
- package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
- package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
- package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
- package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
- package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
- package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
- package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
- package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
- package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
- package/src/api/cyberia-map/cyberia-map.service.js +6 -2
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
- package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
- package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
- package/src/api/file/file.ref.js +153 -0
- package/src/api/file/file.ref.json +11 -2
- package/src/api/object-layer/object-layer.model.js +47 -12
- package/src/api/object-layer/object-layer.router.js +10 -3
- package/src/api/object-layer/object-layer.service.js +113 -120
- package/src/cli/app.js +12 -1
- package/src/cli/client.js +2 -2
- package/src/cli/cluster.js +80 -18
- package/src/cli/deploy.js +215 -34
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/fs-selection.js +115 -0
- package/src/cli/fs.js +158 -342
- package/src/cli/index.js +22 -10
- package/src/cli/release.js +3 -1
- package/src/cli/repository.js +35 -9
- package/src/cli/run.js +80 -199
- package/src/cli/test.js +4 -0
- package/src/cli/wireguard.js +32 -31
- package/src/client/components/core/Badge.js +0 -1
- package/src/client/components/core/CssCore.js +8 -1
- package/src/client/components/core/Docs.js +77 -100
- package/src/client/components/core/Modal.js +77 -23
- package/src/client/components/core/ToolTip.js +15 -0
- package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
- package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
- package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
- package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
- package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
- package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
- package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
- package/src/client/components/default/AppShellDefault.js +1 -4
- package/src/client/components/itemledger/AppShellItemledger.js +0 -2
- package/src/client/components/underpost/AppShellUnderpost.js +1 -1
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
- package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
- package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
- package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
- package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -10
- package/src/client/services/object-layer/object-layer.service.js +3 -1
- package/src/client/ssr/views/Cyberia404.js +3 -3
- package/src/client-builder/client-build-docs.js +52 -52
- package/src/client-builder/client-build.js +67 -44
- package/src/client-builder/client-bundle.js +347 -0
- package/src/db/mongo/MongoExpress.js +187 -0
- package/src/grpc/cyberia/grpc-server.js +5 -7
- package/src/index.js +1 -1
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
- package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
- package/src/projects/cyberia/besu-genesis-generator.js +5 -14
- package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
- package/src/projects/cyberia/catalog-cyberia.js +3 -2
- package/src/projects/cyberia/gemini-client.js +3 -3
- package/src/projects/cyberia/generate-saga.js +4 -11
- package/src/projects/cyberia/hot-reload-trigger.js +17 -28
- package/src/projects/cyberia/instance-backup.js +210 -0
- package/src/projects/cyberia/instance-data.js +177 -257
- package/src/projects/cyberia/map-preview-generator.js +32 -68
- package/src/projects/cyberia/object-layer.js +152 -319
- package/src/projects/cyberia/seed-audio.js +139 -0
- package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
- package/src/projects/cyberia/semantic-layer-generator.js +2 -18
- package/src/projects/cyberia/stat-balance.js +81 -0
- package/src/projects/cyberia/stat-commands.js +23 -0
- package/src/projects/cyberia/stat-contract-generator.js +101 -0
- package/src/runtime/cyberia-client/Dockerfile +8 -4
- package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
- package/src/runtime/cyberia-server/Dockerfile +9 -2
- package/src/runtime/cyberia-server/Dockerfile.dev +8 -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 +2 -2
- package/src/runtime/engine-cyberia/compose.env +17 -5
- package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
- package/src/server/build/coverage.js +113 -42
- package/src/server/build/package.js +55 -12
- package/src/server/build/testing.js +59 -9
- package/src/server/network/middlewares.js +8 -2
- package/src/server/network/underpost-gateway.js +113 -14
- package/src/server/ops/logger.js +70 -35
- package/src/server/runtime/conf.js +40 -9
- package/src/server/runtime/start.js +11 -2
- package/src/server/storage/data-query.js +16 -0
- package/src/server/storage/downloader.js +62 -14
- package/src/server/storage/zip.js +153 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
- package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
- package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
- package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
- package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
- package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
- package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
- package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
- package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
- package/test/integration/app/cyberia/seed-audio.test.js +275 -0
- package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
- package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
- package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
- package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
- package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
- package/test/unit/catalog.test.js +13 -2
- package/test/unit/client-build-docs.test.js +97 -70
- package/test/unit/client-bundle.test.js +313 -0
- package/test/unit/conf-resolution.test.js +58 -7
- package/test/unit/coverage-artifact.test.js +120 -29
- package/test/unit/cyberia/instance-backup.test.js +183 -0
- package/test/unit/cyberia/instance-data.test.js +314 -0
- package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
- package/test/unit/cyberia/publish-workflow.test.js +22 -0
- package/test/unit/cyberia/stat-balance.test.js +69 -0
- package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
- package/test/unit/deploy-log-table.test.js +76 -5
- package/test/unit/downloader-integrity.test.js +98 -0
- package/test/unit/file-reference-registry.test.js +141 -0
- package/test/unit/fs-storage-paths.test.js +596 -0
- package/test/unit/gateway-pod-fetch.test.js +95 -0
- package/test/unit/letsencrypt-issuer.test.js +101 -0
- package/test/unit/logger-redaction.test.js +29 -0
- package/test/unit/package.test.js +70 -16
- package/test/unit/prepare-host.test.js +84 -1
- package/test/unit/release-bump.test.js +3 -1
- package/test/unit/start-options.test.js +12 -0
- package/test/unit/test-tiers.test.js +27 -0
- package/test/unit/zip-archive.test.js +142 -0
- package/vitest.config.js +11 -3
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for the atlas sprite sheet of one object layer item.
|
|
3
|
+
*
|
|
4
|
+
* Owns the AtlasSpriteSheet document, the File blobs it points at, and the
|
|
5
|
+
* IPFS pins of the atlas PNG and its metadata. Every writer — the REST service
|
|
6
|
+
* and the Cyberia CLI — goes through here, so one item key always resolves to
|
|
7
|
+
* one atlas.
|
|
8
|
+
*
|
|
9
|
+
* @module src/projects/cyberia/atlas-sprite-sheet-store.js
|
|
10
|
+
* @namespace CyberiaAtlasSpriteSheetStore
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import { Types } from 'mongoose';
|
|
15
|
+
import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
|
|
16
|
+
import { loggerFactory } from '../../server/ops/logger.js';
|
|
17
|
+
import { createPinRecord } from '../../api/ipfs/ipfs.service.js';
|
|
18
|
+
import { FileFactory } from '../../api/file/file.service.js';
|
|
19
|
+
import { deleteOwnedFiles, documentFileIds, fileRefFields } from '../../api/file/file.ref.js';
|
|
20
|
+
import { AtlasSpriteSheetGenerator, DEFAULT_ATLAS_UPSCALE_FACTOR } from './atlas-sprite-sheet-generator.js';
|
|
21
|
+
import { IpfsClient } from './ipfs-client.js';
|
|
22
|
+
|
|
23
|
+
const logger = loggerFactory(import.meta);
|
|
24
|
+
|
|
25
|
+
/** The File-referencing fields of this model, read from the registry that owns that mapping. */
|
|
26
|
+
export const ATLAS_FILE_FIELDS = fileRefFields('atlas-sprite-sheet');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The File `name` an atlas render is written under, as {@link upsertRenderFile} spells it.
|
|
30
|
+
* A prune reads it to recognise a render whose atlas is already gone.
|
|
31
|
+
* @constant {RegExp}
|
|
32
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
33
|
+
*/
|
|
34
|
+
const ATLAS_RENDER_NAME = /-(atlas|minify|idle)\.png$/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* MFS paths of the IPFS content an item key owns.
|
|
38
|
+
* @param {string} itemKey - Object layer item id.
|
|
39
|
+
* @returns {{ png: string, metadata: string }}
|
|
40
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
41
|
+
*/
|
|
42
|
+
export const atlasMfsPaths = (itemKey) => ({
|
|
43
|
+
png: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
44
|
+
metadata: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The File `_id` a render resolves through, derived from the item key, the render
|
|
49
|
+
* role and the bytes themselves.
|
|
50
|
+
*
|
|
51
|
+
* Content-addressed on purpose: identical bytes re-derive the same id, which makes
|
|
52
|
+
* a rerun a no-op instead of a churn of File documents. Changed bytes land on a new
|
|
53
|
+
* id, and the writer deletes the one it replaced.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} itemKey - Object layer item id.
|
|
56
|
+
* @param {string} role - Render role: `atlas`, `minify` or `idle`.
|
|
57
|
+
* @param {string} md5 - Hex MD5 of the PNG bytes.
|
|
58
|
+
* @returns {import('mongoose').Types.ObjectId}
|
|
59
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
60
|
+
*/
|
|
61
|
+
const atlasFileId = (itemKey, role, md5) =>
|
|
62
|
+
new Types.ObjectId(crypto.createHash('sha256').update(`atlas:${itemKey}:${role}:${md5}`).digest('hex').slice(0, 24));
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Writes a PNG render to the File collection under its content-addressed id.
|
|
66
|
+
* Re-writing the same bytes returns the stored document untouched.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} File - Mongoose File model.
|
|
69
|
+
* @param {string} itemKey - Object layer item id.
|
|
70
|
+
* @param {string} role - Render role: `atlas`, `minify` or `idle`.
|
|
71
|
+
* @param {Buffer} buffer - PNG bytes.
|
|
72
|
+
* @returns {Promise<import('mongoose').Types.ObjectId>} The File id.
|
|
73
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
74
|
+
*/
|
|
75
|
+
const upsertRenderFile = async (File, itemKey, role, buffer) => {
|
|
76
|
+
const payload = FileFactory.create(buffer, `${itemKey}-${role}.png`);
|
|
77
|
+
const _id = atlasFileId(itemKey, role, payload.md5);
|
|
78
|
+
await File.updateOne({ _id }, { $setOnInsert: { ...payload, _id } }, { upsert: true });
|
|
79
|
+
return _id;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Writes the idle-preview still cut from a human-resolution render, or returns null
|
|
84
|
+
* for an atlas with no frame to cut.
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} File - Mongoose File model.
|
|
87
|
+
* @param {string} itemKey - Object layer item id.
|
|
88
|
+
* @param {Buffer} buffer - PNG bytes of the human-resolution render.
|
|
89
|
+
* @param {Object} metadata - Atlas metadata describing that render's layout.
|
|
90
|
+
* @returns {Promise<import('mongoose').Types.ObjectId|null>}
|
|
91
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
92
|
+
*/
|
|
93
|
+
const upsertIdlePreview = async (File, itemKey, buffer, metadata) => {
|
|
94
|
+
const still = await AtlasSpriteSheetGenerator.idlePreviewFromAtlas(buffer, metadata);
|
|
95
|
+
return still ? await upsertRenderFile(File, itemKey, 'idle', still) : null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Deletes the File documents an atlas no longer points at.
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} File - Mongoose File model.
|
|
102
|
+
* @param {Array<import('mongoose').Types.ObjectId>} previousIds - File ids held before the write.
|
|
103
|
+
* @param {Array<import('mongoose').Types.ObjectId>} currentIds - File ids held after the write.
|
|
104
|
+
* @returns {Promise<void>}
|
|
105
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
106
|
+
*/
|
|
107
|
+
const deleteReplacedFiles = async (File, previousIds, currentIds) => {
|
|
108
|
+
const kept = new Set(currentIds.filter(Boolean).map(String));
|
|
109
|
+
const replaced = [...new Set(previousIds.filter(Boolean).map(String))].filter((id) => !kept.has(id));
|
|
110
|
+
if (replaced.length === 0) return;
|
|
111
|
+
await File.deleteMany({ _id: { $in: replaced } });
|
|
112
|
+
logger.info(`Removed ${replaced.length} replaced atlas File document(s)`);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Pins a payload to IPFS and records it in the registry. A failure is logged and
|
|
117
|
+
* yields an empty CID, so a missing IPFS node never blocks a write.
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} params
|
|
120
|
+
* @param {function(): Promise<{cid: string}|null>} params.add - The IpfsClient call.
|
|
121
|
+
* @param {string} params.resourceType - Registry category.
|
|
122
|
+
* @param {string} params.mfsPath - MFS path of the payload.
|
|
123
|
+
* @param {Object} [params.options] - Router options ({ host, path }).
|
|
124
|
+
* @returns {Promise<string>} The CID, or an empty string.
|
|
125
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
126
|
+
*/
|
|
127
|
+
const pin = async ({ add, resourceType, mfsPath, options }) => {
|
|
128
|
+
try {
|
|
129
|
+
const result = await add();
|
|
130
|
+
if (!result) return '';
|
|
131
|
+
await createPinRecord({ cid: result.cid, resourceType, mfsPath, options });
|
|
132
|
+
logger.info(`Pinned ${resourceType} – CID: ${result.cid}`);
|
|
133
|
+
return result.cid;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.warn(`Failed to pin ${resourceType}: ${error.message}`);
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Tells whether two atlas layouts place every frame at the same cell.
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} left - Atlas metadata.
|
|
144
|
+
* @param {Object} right - Atlas metadata.
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
147
|
+
*/
|
|
148
|
+
const sameLayout = (left, right) => {
|
|
149
|
+
if (left.atlasWidth !== right.atlasWidth || left.atlasHeight !== right.atlasHeight) return false;
|
|
150
|
+
const box = (frame) => (frame ? `${frame.x},${frame.y},${frame.width},${frame.height}` : '');
|
|
151
|
+
const directions = new Set([...Object.keys(left.frames ?? {}), ...Object.keys(right.frames ?? {})]);
|
|
152
|
+
for (const direction of directions) {
|
|
153
|
+
const leftFrames = left.frames?.[direction] ?? [];
|
|
154
|
+
const rightFrames = right.frames?.[direction] ?? [];
|
|
155
|
+
if (leftFrames.length !== rightFrames.length) return false;
|
|
156
|
+
for (let index = 0; index < leftFrames.length; index++) {
|
|
157
|
+
if (box(leftFrames[index]) !== box(rightFrames[index])) return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Atlas sprite sheet persistence, keyed by object layer item id.
|
|
165
|
+
* @class AtlasSpriteSheetStore
|
|
166
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
167
|
+
*/
|
|
168
|
+
export class AtlasSpriteSheetStore {
|
|
169
|
+
/**
|
|
170
|
+
* Generates both renders of an item's atlas and stores them.
|
|
171
|
+
*
|
|
172
|
+
* The write is keyed by `metadata.itemKey`, so a rerun updates the same
|
|
173
|
+
* document. Renders whose bytes did not change keep their File documents;
|
|
174
|
+
* renders that changed replace them and the old ones are deleted.
|
|
175
|
+
*
|
|
176
|
+
* @static
|
|
177
|
+
* @param {Object} params
|
|
178
|
+
* @param {string} params.itemKey - Object layer item id.
|
|
179
|
+
* @param {Object} params.objectLayerRenderFrames - Render frames, document or plain object.
|
|
180
|
+
* @param {number} [params.upscaleFactor=DEFAULT_ATLAS_UPSCALE_FACTOR] - Pixels per cell of the human-resolution render.
|
|
181
|
+
* @param {number} [params.maxAtlasDim=null] - Maximum atlas dimension, auto-calculated when null.
|
|
182
|
+
* @param {Object} [params.options] - Router options ({ host, path }) for model lookup.
|
|
183
|
+
* @returns {Promise<{atlasDoc: Object, metadata: Object, atlasCid: string, atlasMetadataCid: string}>}
|
|
184
|
+
* `metadata` is the plain generated layout, free of the document wrapper a caller would have to unwrap.
|
|
185
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
186
|
+
*/
|
|
187
|
+
static async persist({
|
|
188
|
+
itemKey,
|
|
189
|
+
objectLayerRenderFrames,
|
|
190
|
+
upscaleFactor = DEFAULT_ATLAS_UPSCALE_FACTOR,
|
|
191
|
+
maxAtlasDim = null,
|
|
192
|
+
options,
|
|
193
|
+
}) {
|
|
194
|
+
const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
|
|
195
|
+
const File = DataBaseProviderService.getModel('File', options);
|
|
196
|
+
|
|
197
|
+
const { buffer, minifyBuffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
198
|
+
objectLayerRenderFrames,
|
|
199
|
+
itemKey,
|
|
200
|
+
upscaleFactor,
|
|
201
|
+
maxAtlasDim,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const fileId = await upsertRenderFile(File, itemKey, 'atlas', buffer);
|
|
205
|
+
const minifyFileId = await upsertRenderFile(File, itemKey, 'minify', minifyBuffer);
|
|
206
|
+
const idlePreviewFileId = await upsertIdlePreview(File, itemKey, buffer, metadata);
|
|
207
|
+
|
|
208
|
+
const mfsPaths = atlasMfsPaths(itemKey);
|
|
209
|
+
const atlasCid = await pin({
|
|
210
|
+
add: () => IpfsClient.addBufferToIpfs(buffer, `${itemKey}_atlas_sprite_sheet.png`, mfsPaths.png),
|
|
211
|
+
resourceType: 'atlas-sprite-sheet',
|
|
212
|
+
mfsPath: mfsPaths.png,
|
|
213
|
+
options,
|
|
214
|
+
});
|
|
215
|
+
const atlasMetadataCid = await pin({
|
|
216
|
+
add: () => IpfsClient.addJsonToIpfs(metadata, `${itemKey}_atlas_sprite_sheet_metadata.json`, mfsPaths.metadata),
|
|
217
|
+
resourceType: 'atlas-metadata',
|
|
218
|
+
mfsPath: mfsPaths.metadata,
|
|
219
|
+
options,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
223
|
+
const previousFileIds = atlasDoc ? ATLAS_FILE_FIELDS.map((field) => atlasDoc[field]) : [];
|
|
224
|
+
|
|
225
|
+
const renders = { fileId, minifyFileId, idlePreviewFileId };
|
|
226
|
+
if (atlasDoc) {
|
|
227
|
+
atlasDoc.set({ ...renders, cid: atlasCid, metadata });
|
|
228
|
+
await atlasDoc.save();
|
|
229
|
+
} else {
|
|
230
|
+
atlasDoc = await new AtlasSpriteSheet({ ...renders, cid: atlasCid, metadata }).save();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await deleteReplacedFiles(File, previousFileIds, Object.values(renders));
|
|
234
|
+
|
|
235
|
+
return { atlasDoc, metadata, atlasCid, atlasMetadataCid };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Removes atlases and everything they own: every File render they point at, and
|
|
240
|
+
* the documents themselves.
|
|
241
|
+
*
|
|
242
|
+
* The one place a caller deletes an atlas, because an atlas owns more than its
|
|
243
|
+
* document: a drop that took `fileId` and left `minifyFileId` behind is what put
|
|
244
|
+
* unreachable renders in the File collection. Rerunnable — a second call finds
|
|
245
|
+
* nothing and reports zeros.
|
|
246
|
+
*
|
|
247
|
+
* @static
|
|
248
|
+
* @param {Object} params
|
|
249
|
+
* @param {string[]} [params.itemKeys] - Item keys to purge.
|
|
250
|
+
* @param {Array<*>} [params.atlasIds] - Atlas document ids to purge, for a caller holding the link rather than the key.
|
|
251
|
+
* @param {boolean} [params.all=false] - Purge the whole collection, ignoring both selectors.
|
|
252
|
+
* @param {Object} [params.options] - Router options ({ host, path }) for model lookup.
|
|
253
|
+
* @returns {Promise<{atlases: number, files: number, cids: string[]}>} What was removed, and the CIDs the caller still has to unpin.
|
|
254
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
255
|
+
*/
|
|
256
|
+
static async purge({ itemKeys = [], atlasIds = [], all = false, options }) {
|
|
257
|
+
const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
|
|
258
|
+
const File = DataBaseProviderService.getModel('File', options);
|
|
259
|
+
|
|
260
|
+
const clauses = [];
|
|
261
|
+
if (itemKeys.length > 0) clauses.push({ 'metadata.itemKey': { $in: itemKeys } });
|
|
262
|
+
if (atlasIds.length > 0) clauses.push({ _id: { $in: atlasIds } });
|
|
263
|
+
if (!all && clauses.length === 0) return { atlases: 0, files: 0, cids: [] };
|
|
264
|
+
const selector = all ? {} : clauses.length === 1 ? clauses[0] : { $or: clauses };
|
|
265
|
+
|
|
266
|
+
const docs = await AtlasSpriteSheet.find(
|
|
267
|
+
selector,
|
|
268
|
+
Object.fromEntries([...ATLAS_FILE_FIELDS, 'cid'].map((field) => [field, 1])),
|
|
269
|
+
).lean();
|
|
270
|
+
if (docs.length === 0) return { atlases: 0, files: 0, cids: [] };
|
|
271
|
+
|
|
272
|
+
const fileIds = documentFileIds(docs, ATLAS_FILE_FIELDS);
|
|
273
|
+
const cids = [...new Set(docs.map((doc) => doc.cid).filter(Boolean))];
|
|
274
|
+
|
|
275
|
+
// Documents first: the survivors this reads decide which renders are still
|
|
276
|
+
// reachable, so the ones being dropped must already be gone.
|
|
277
|
+
const { deletedCount } = await AtlasSpriteSheet.deleteMany({ _id: { $in: docs.map((doc) => doc._id) } });
|
|
278
|
+
const files = await deleteOwnedFiles({ File, Owner: AtlasSpriteSheet, fields: ATLAS_FILE_FIELDS, ids: fileIds });
|
|
279
|
+
|
|
280
|
+
return { atlases: deletedCount ?? 0, files, cids };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Deletes atlas renders no atlas document points at any more.
|
|
285
|
+
*
|
|
286
|
+
* The repair for what an earlier drop left behind: candidates are recognised by
|
|
287
|
+
* the name the writer gives a render, and a candidate a live atlas still holds
|
|
288
|
+
* is kept. Idempotent, and a no-op on a collection that never leaked.
|
|
289
|
+
*
|
|
290
|
+
* @static
|
|
291
|
+
* @param {Object} params
|
|
292
|
+
* @param {Object} [params.options] - Router options ({ host, path }) for model lookup.
|
|
293
|
+
* @returns {Promise<number>} How many orphaned renders were removed.
|
|
294
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
295
|
+
*/
|
|
296
|
+
static async pruneOrphanRenders({ options } = {}) {
|
|
297
|
+
const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
|
|
298
|
+
const File = DataBaseProviderService.getModel('File', options);
|
|
299
|
+
|
|
300
|
+
const candidates = await File.find({ name: { $regex: ATLAS_RENDER_NAME } }, { _id: 1 }).lean();
|
|
301
|
+
if (candidates.length === 0) return 0;
|
|
302
|
+
|
|
303
|
+
const removed = await deleteOwnedFiles({
|
|
304
|
+
File,
|
|
305
|
+
Owner: AtlasSpriteSheet,
|
|
306
|
+
fields: ATLAS_FILE_FIELDS,
|
|
307
|
+
ids: candidates.map((doc) => doc._id),
|
|
308
|
+
});
|
|
309
|
+
if (removed > 0) logger.info(`Removed ${removed} orphaned atlas File document(s)`);
|
|
310
|
+
return removed;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Refreshes only the minified render of a stored atlas.
|
|
315
|
+
*
|
|
316
|
+
* The layout must still match the stored metadata, because the client runtime
|
|
317
|
+
* pairs `GET /blob/:itemKey` with `GET /metadata/:itemKey`. A layout that moved
|
|
318
|
+
* means the render frames changed after the atlas was generated, so the item is
|
|
319
|
+
* reported as stale instead of written.
|
|
320
|
+
*
|
|
321
|
+
* @static
|
|
322
|
+
* @param {Object} params
|
|
323
|
+
* @param {string} params.itemKey - Object layer item id.
|
|
324
|
+
* @param {Object} params.objectLayerRenderFrames - Render frames, document or plain object.
|
|
325
|
+
* @param {Object} [params.options] - Router options ({ host, path }) for model lookup.
|
|
326
|
+
* @returns {Promise<{ status: 'updated'|'unchanged'|'missing'|'stale', minifyFileId?: import('mongoose').Types.ObjectId }>}
|
|
327
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
328
|
+
*/
|
|
329
|
+
static async syncMinifyRender({ itemKey, objectLayerRenderFrames, options }) {
|
|
330
|
+
const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
|
|
331
|
+
const File = DataBaseProviderService.getModel('File', options);
|
|
332
|
+
|
|
333
|
+
const atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
334
|
+
if (!atlasDoc) return { status: 'missing' };
|
|
335
|
+
|
|
336
|
+
// Only the minified render is wanted here, so ask for no upscaled second render.
|
|
337
|
+
const { minifyBuffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
338
|
+
objectLayerRenderFrames,
|
|
339
|
+
itemKey,
|
|
340
|
+
1,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
if (!sameLayout(metadata, atlasDoc.metadata.toObject ? atlasDoc.metadata.toObject() : atlasDoc.metadata)) {
|
|
344
|
+
return { status: 'stale' };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const previousId = atlasDoc.minifyFileId;
|
|
348
|
+
const minifyFileId = await upsertRenderFile(File, itemKey, 'minify', minifyBuffer);
|
|
349
|
+
if (previousId && String(previousId) === String(minifyFileId)) {
|
|
350
|
+
return { status: 'unchanged', minifyFileId };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
await AtlasSpriteSheet.updateOne({ _id: atlasDoc._id }, { $set: { minifyFileId } });
|
|
354
|
+
await deleteReplacedFiles(File, [previousId], [minifyFileId]);
|
|
355
|
+
|
|
356
|
+
return { status: 'updated', minifyFileId };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Fills or refreshes the idle-preview still of a stored atlas from the render it already
|
|
361
|
+
* holds, so an atlas written before the still existed gains one without a regeneration.
|
|
362
|
+
*
|
|
363
|
+
* @static
|
|
364
|
+
* @param {Object} params
|
|
365
|
+
* @param {string} params.itemKey - Object layer item id.
|
|
366
|
+
* @param {Object} [params.options] - Router options ({ host, path }) for model lookup.
|
|
367
|
+
* @returns {Promise<{ status: 'updated'|'unchanged'|'missing', idlePreviewFileId?: import('mongoose').Types.ObjectId }>}
|
|
368
|
+
* @memberof CyberiaAtlasSpriteSheetStore
|
|
369
|
+
*/
|
|
370
|
+
static async syncIdlePreview({ itemKey, options }) {
|
|
371
|
+
const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
|
|
372
|
+
const File = DataBaseProviderService.getModel('File', options);
|
|
373
|
+
|
|
374
|
+
const atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
375
|
+
// Hydrated, not lean: a lean read hands back a BSON Binary the decoder cannot open.
|
|
376
|
+
const render = atlasDoc?.fileId ? await File.findById(atlasDoc.fileId) : null;
|
|
377
|
+
if (!render?.data) return { status: 'missing' };
|
|
378
|
+
|
|
379
|
+
const metadata = atlasDoc.metadata.toObject ? atlasDoc.metadata.toObject() : atlasDoc.metadata;
|
|
380
|
+
const previousId = atlasDoc.idlePreviewFileId;
|
|
381
|
+
const idlePreviewFileId = await upsertIdlePreview(File, itemKey, Buffer.from(render.data), metadata);
|
|
382
|
+
if (previousId && String(previousId) === String(idlePreviewFileId)) {
|
|
383
|
+
return { status: 'unchanged', idlePreviewFileId };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
await AtlasSpriteSheet.updateOne({ _id: atlasDoc._id }, { $set: { idlePreviewFileId } });
|
|
387
|
+
await deleteReplacedFiles(File, [previousId], [idlePreviewFileId]);
|
|
388
|
+
|
|
389
|
+
return { status: 'updated', idlePreviewFileId };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -61,26 +61,17 @@ function derivePublicKey(privateKeyHex) {
|
|
|
61
61
|
* (uncompressed, sans 04 prefix) public key bytes:
|
|
62
62
|
* `address = keccak256(pubKeyBytes)[12..31]`
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* produces **wrong** addresses. We use `ethers.keccak256` which bundles
|
|
68
|
-
* a correct Keccak-256 implementation.
|
|
64
|
+
* Ethereum uses the original Keccak-256, not NIST SHA3-256. NIST added other
|
|
65
|
+
* domain-separation padding, so `crypto.createHash('sha3-256')` gives wrong
|
|
66
|
+
* addresses. `ethers.keccak256` carries a correct Keccak-256.
|
|
69
67
|
*
|
|
70
68
|
* @param {string} publicKeyHex - 128-char hex public key (no 0x prefix).
|
|
71
69
|
* @returns {string} 40-char lowercase hex Ethereum address (no 0x prefix).
|
|
72
70
|
* @memberof BesuGenesisGenerator
|
|
73
71
|
*/
|
|
74
72
|
function publicKeyToAddress(publicKeyHex) {
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// so crypto.createHash('sha3-256') produces WRONG addresses.
|
|
78
|
-
//
|
|
79
|
-
// Node.js's OpenSSL does not expose the original Keccak-256, so we use ethers
|
|
80
|
-
// which bundles a correct implementation.
|
|
81
|
-
//
|
|
82
|
-
// ethersKeccak256 expects a 0x-prefixed hex string or Uint8Array and returns
|
|
83
|
-
// a 0x-prefixed 64-char hex hash. The Ethereum address is the last 20 bytes.
|
|
73
|
+
// ethersKeccak256 takes 0x-prefixed hex or a Uint8Array and returns a
|
|
74
|
+
// 0x-prefixed 64-char hash. The address is its last 20 bytes.
|
|
84
75
|
const hash = ethersKeccak256(Buffer.from(publicKeyHex, 'hex')); // 0x-prefixed 64-char hex
|
|
85
76
|
return hash.slice(26); // skip '0x' + first 24 hex chars → last 40 hex chars (20 bytes)
|
|
86
77
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { STAT_DEFAULTS } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
|
|
2
|
+
import { DefaultCyberiaActions, DefaultCyberiaQuests } from '../../api/cyberia-server-defaults/cyberia-server-defaults.js';
|
|
3
|
+
import { toInstanceConfig, toMapMsg, toInstanceMsg, toObjectLayerMsg, toActionMsg, toQuestMsg } from './instance-data.js';
|
|
4
|
+
|
|
5
|
+
export function buildBootContractArtifacts() {
|
|
6
|
+
const layer = toObjectLayerMsg({
|
|
7
|
+
_id: 'contract-layer', sha256: 'abc',
|
|
8
|
+
data: { stats: { ...STAT_DEFAULTS, effect: -100, resistance: 100 }, item: { id: 'sword', type: 'weapon' } },
|
|
9
|
+
});
|
|
10
|
+
const full = {
|
|
11
|
+
instance: toInstanceMsg({ _id: 'contract-instance', code: 'contract-test', cyberiaMapCodes: ['contract-map'] }),
|
|
12
|
+
maps: [toMapMsg({
|
|
13
|
+
_id: 'contract-map', code: 'contract-map', gridX: 16, gridY: 16,
|
|
14
|
+
entities: [{ entityType: 'bot', level: 7, objectLayerItemIds: ['sword'] }],
|
|
15
|
+
})],
|
|
16
|
+
objectLayers: [layer],
|
|
17
|
+
config: toInstanceConfig({}),
|
|
18
|
+
version: 'contract-fixture',
|
|
19
|
+
actions: DefaultCyberiaActions.slice(0, 1).map(toActionMsg),
|
|
20
|
+
quests: DefaultCyberiaQuests.slice(0, 1).map(toQuestMsg),
|
|
21
|
+
};
|
|
22
|
+
const payloads = {
|
|
23
|
+
boot_full_instance: full,
|
|
24
|
+
boot_object_layer: layer,
|
|
25
|
+
boot_manifest: { entries: [{ itemId: 'sword', sha256: 'abc' }] },
|
|
26
|
+
boot_ping: { serverTimeMs: 1000 },
|
|
27
|
+
};
|
|
28
|
+
return Object.fromEntries(Object.entries(payloads).map(([name, data]) => [
|
|
29
|
+
'cyberia-server/engine_client/testdata/' + name + '.json',
|
|
30
|
+
JSON.stringify({ status: 'success', data }, null, 2) + '\n',
|
|
31
|
+
]));
|
|
32
|
+
}
|
|
@@ -39,11 +39,10 @@ export default {
|
|
|
39
39
|
packageBin: { cyberia: 'bin/index.js', underpost: 'bin/index.js' },
|
|
40
40
|
// Native-dependency pin list: versions stay reproducible across CI and production deploys.
|
|
41
41
|
packageDependencies: {
|
|
42
|
-
'adm-zip': '^0.6.0',
|
|
43
42
|
'maxrects-packer': '^2.7.3',
|
|
44
43
|
pngjs: '^7.0.0',
|
|
45
44
|
jimp: '^1.6.0',
|
|
46
|
-
sharp: '^0.35.
|
|
45
|
+
sharp: '^0.35.4',
|
|
47
46
|
ethers: '~6.16.0',
|
|
48
47
|
},
|
|
49
48
|
packageScripts: {
|
|
@@ -73,6 +72,7 @@ export default {
|
|
|
73
72
|
'/src/grpc/cyberia',
|
|
74
73
|
'/src/client/ssr/views/CyberiaServerMetrics.js',
|
|
75
74
|
'/src/client/ssr/views/Cyberia404.js',
|
|
75
|
+
'/test/unit/cyberia',
|
|
76
76
|
'/test/integration/app/cyberia',
|
|
77
77
|
'/src/projects/cyberia',
|
|
78
78
|
'/src/runtime/cyberia-server',
|
|
@@ -102,6 +102,7 @@ export default {
|
|
|
102
102
|
'./src/runtime/cyberia-server',
|
|
103
103
|
'./src/runtime/cyberia-client',
|
|
104
104
|
'./src/runtime/engine-cyberia',
|
|
105
|
+
'./test/unit/cyberia',
|
|
105
106
|
'./test/integration/app/cyberia',
|
|
106
107
|
'./src/client/public/cyberia-docs',
|
|
107
108
|
'./bin/cyberia.js',
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Top-Down PCG saga generator. No streaming, no tool-calls — the saga
|
|
6
6
|
* orchestration only needs one structured JSON payload per request.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Only the `:generateContent` endpoint is wired, matching the Gemma model
|
|
9
|
+
* family (e.g. `gemma-4-26b-a4b-it`). Authentication uses the `x-goog-api-key`
|
|
10
|
+
* header from `GEMINI_API_KEY`.
|
|
11
11
|
*
|
|
12
12
|
* @module src/projects/cyberia/gemini-client.js
|
|
13
13
|
* @namespace GeminiClient
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateStats } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
|
|
1
2
|
/**
|
|
2
3
|
* Top-Down Procedural Generation guided by LLMs (Semantic Reverse-Engineering).
|
|
3
4
|
*
|
|
@@ -1089,9 +1090,10 @@ const STAGE_PROMPTS = {
|
|
|
1089
1090
|
'STAGE: foundation. Return ONE JSON object: { "saga": {...}, "objectLayers": [...] }.',
|
|
1090
1091
|
'saga: { code, name, description, mapCodes:[] } (leave mapCodes an empty array).',
|
|
1091
1092
|
'objectLayers[]: { stats:{ effect, resistance, agility, range, intelligence, utility },',
|
|
1093
|
+
'- Stats are integer modifiers from -100 to +100. Zero is neutral. Level and XP belong to entities.',
|
|
1092
1094
|
' item:{ id, type, description, activable }, render:null }',
|
|
1093
1095
|
' item.type is one of: skin, breastplate, weapon, skill, coin, floor, obstacle, portal,',
|
|
1094
|
-
' foreground, resource; every stat is an integer
|
|
1096
|
+
' foreground, resource; every stat is an integer -100..+100, with 0 neutral.',
|
|
1095
1097
|
'Produce 5-10 object-layer items including at least one "coin" currency item AND at least two',
|
|
1096
1098
|
'"skin" items that represent NAMED NPC characters players can speak to (these back "talk" quests).',
|
|
1097
1099
|
].join('\n'),
|
|
@@ -1536,14 +1538,7 @@ function normalizeSagaPayload(raw, { theme }) {
|
|
|
1536
1538
|
const objectLayers = asArray(raw.objectLayers).map((ol) => {
|
|
1537
1539
|
const stats = ol.stats || {};
|
|
1538
1540
|
return {
|
|
1539
|
-
stats:
|
|
1540
|
-
effect: Number(stats.effect) || 0,
|
|
1541
|
-
resistance: Number(stats.resistance) || 0,
|
|
1542
|
-
agility: Number(stats.agility) || 0,
|
|
1543
|
-
range: Number(stats.range) || 0,
|
|
1544
|
-
intelligence: Number(stats.intelligence) || 0,
|
|
1545
|
-
utility: Number(stats.utility) || 0,
|
|
1546
|
-
},
|
|
1541
|
+
stats: validateStats(stats),
|
|
1547
1542
|
item: {
|
|
1548
1543
|
id: slugify(ol.item?.id),
|
|
1549
1544
|
type: ol.item?.type || 'skin',
|
|
@@ -1621,7 +1616,6 @@ function normalizeSagaPayload(raw, { theme }) {
|
|
|
1621
1616
|
description: saga.description,
|
|
1622
1617
|
tags: ['saga', 'generated'],
|
|
1623
1618
|
cyberiaMapCodes: [...saga.mapCodes],
|
|
1624
|
-
itemIds: saga.itemIds.map((id) => ({ id, defaultPlayerInventory: false })),
|
|
1625
1619
|
portals: [],
|
|
1626
1620
|
topologyMode: 'procedural',
|
|
1627
1621
|
};
|
|
@@ -1687,7 +1681,6 @@ async function persistInstance({ instance, CyberiaInstance }) {
|
|
|
1687
1681
|
description: instance.description,
|
|
1688
1682
|
tags: instance.tags,
|
|
1689
1683
|
cyberiaMapCodes: instance.cyberiaMapCodes,
|
|
1690
|
-
itemIds: instance.itemIds,
|
|
1691
1684
|
topologyMode: instance.topologyMode,
|
|
1692
1685
|
},
|
|
1693
1686
|
$setOnInsert: { code: instance.code, portals: instance.portals || [] },
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hot-reload trigger — engine
|
|
2
|
+
* Hot-reload trigger — the engine's control channel to a running game server.
|
|
3
3
|
*
|
|
4
|
-
* Asks
|
|
5
|
-
*
|
|
6
|
-
* order:
|
|
4
|
+
* Asks the server to rebuild its world now instead of waiting for its polling
|
|
5
|
+
* loop. Two transports, in order:
|
|
7
6
|
*
|
|
8
|
-
* 1. gRPC
|
|
9
|
-
* 2. REST
|
|
10
|
-
*
|
|
7
|
+
* 1. gRPC, the server's CyberiaControlService. Preferred.
|
|
8
|
+
* 2. REST, POST {baseUrl}/api/v1/hot-reload. Used when the control gRPC port
|
|
9
|
+
* is unreachable.
|
|
11
10
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* The control service uses a JSON codec, so this client is hand-rolled with
|
|
12
|
+
* makeUnaryRequest and JSON serializers. There is no .proto codegen; SERVICE
|
|
13
|
+
* and METHOD must match the server's control service.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Both transports carry CYBERIA_SERVER_API_KEY, the INTERNAL shared secret
|
|
19
|
-
* between engine-cyberia and cyberia-server. It is read from the environment
|
|
20
|
-
* here and never returned to a browser: the engine's REST endpoint is the only
|
|
21
|
-
* public surface, and it is moderator/admin guarded.
|
|
15
|
+
* Both transports carry CYBERIA_SERVER_API_KEY, the internal shared secret. It
|
|
16
|
+
* is read from the environment and never returned to a browser.
|
|
22
17
|
*
|
|
23
18
|
* @module src/projects/cyberia/hot-reload-trigger.js
|
|
24
19
|
*/
|
|
@@ -49,11 +44,9 @@ const serverApiKey = () => process.env.CYBERIA_SERVER_API_KEY || '';
|
|
|
49
44
|
* gRPC target. The gRPC host reuses the hostname with the control port, since
|
|
50
45
|
* the control service listens on its own port.
|
|
51
46
|
*
|
|
52
|
-
* The URL sub-path is
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* Using `url.origin` alone would drop the sub-path and land every trigger on the
|
|
56
|
-
* default (root) deployment.
|
|
47
|
+
* The URL sub-path is kept as `basePath` ("", "/FOREST", "/TEST") and stays on
|
|
48
|
+
* `restBaseUrl`. A path-based multi-instance proxy routes the REST trigger by
|
|
49
|
+
* it; `url.origin` alone would send every trigger to the root deployment.
|
|
57
50
|
*/
|
|
58
51
|
const resolveTargets = (rawUrl, { grpcPort = DEFAULT_GRPC_PORT } = {}) => {
|
|
59
52
|
const trimmed = String(rawUrl || '').trim();
|
|
@@ -133,11 +126,8 @@ const triggerHotReload = async ({
|
|
|
133
126
|
const { restBaseUrl, grpcTarget, basePath } = resolveTargets(serverUrl, { grpcPort });
|
|
134
127
|
const call = { apiKey, mode, instanceCode, timeoutMs };
|
|
135
128
|
|
|
136
|
-
// A path
|
|
137
|
-
//
|
|
138
|
-
// /FOREST prefix — a control call to hostname:port reaches the default
|
|
139
|
-
// variant. When the target names a variant sub-path, use the sub-path-aware
|
|
140
|
-
// REST transport directly instead of triggering the wrong world over gRPC.
|
|
129
|
+
// A gRPC method path is fixed and cannot carry the variant sub-path, so a
|
|
130
|
+
// control call would reach the default variant. Use REST for a sub-path target.
|
|
141
131
|
if (basePath) {
|
|
142
132
|
const result = await triggerViaRest({ ...call, restBaseUrl });
|
|
143
133
|
logger.info(`hot reload via REST ${restBaseUrl}: ${result?.message}`);
|
|
@@ -149,8 +139,7 @@ const triggerHotReload = async ({
|
|
|
149
139
|
logger.info(`hot reload via gRPC ${grpcTarget}: ${result?.message}`);
|
|
150
140
|
return { transport: 'grpc', result };
|
|
151
141
|
} catch (grpcError) {
|
|
152
|
-
// A rejected key is
|
|
153
|
-
// over REST, or a misconfigured key would be reported twice.
|
|
142
|
+
// A rejected key is an answer, not a transport failure. Do not retry.
|
|
154
143
|
if (grpcError?.code === grpc.status.PERMISSION_DENIED) throw new Error(grpcError.details || grpcError.message);
|
|
155
144
|
logger.warn(`hot reload gRPC (${grpcTarget}) unavailable: ${grpcError.message} — falling back to REST`);
|
|
156
145
|
|