@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
|
@@ -1,56 +1,19 @@
|
|
|
1
|
+
import { STAT_TYPES, STAT_MODIFIER_MAX } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
|
|
1
2
|
import { Schema, model } from 'mongoose';
|
|
2
|
-
import { CYBERIA_INSTANCE_CONF_DEFAULTS as D } from '../cyberia-server-defaults/cyberia-server-defaults.js';
|
|
3
|
+
import { CYBERIA_INSTANCE_CONF_DEFAULTS as D, resolveProgressionRules, PROGRESSION_RULE_LIMITS } from '../cyberia-server-defaults/cyberia-server-defaults.js';
|
|
4
|
+
|
|
5
|
+
const BaseStatsSchema = new Schema(Object.fromEntries(STAT_TYPES.map((key) => [key, {
|
|
6
|
+
type: Number, min: 0, max: STAT_MODIFIER_MAX, validate: Number.isInteger,
|
|
7
|
+
}])), { _id: false, strict: 'throw' });
|
|
8
|
+
const ProgressionRulesSchema = new Schema({
|
|
9
|
+
...Object.fromEntries(Object.entries(PROGRESSION_RULE_LIMITS).map(([key, [min, max]]) => [key, {
|
|
10
|
+
type: Number, min, max, validate: Number.isInteger,
|
|
11
|
+
}])),
|
|
12
|
+
baseStats: BaseStatsSchema,
|
|
13
|
+
perLevelStats: BaseStatsSchema,
|
|
14
|
+
}, { _id: false, strict: 'throw' });
|
|
15
|
+
ProgressionRulesSchema.pre('validate', function () { resolveProgressionRules(this.toObject()); });
|
|
3
16
|
|
|
4
|
-
// ObjectLayer inventory slot: itemId + whether it starts active + initial quantity.
|
|
5
|
-
// Used by EntityDefaultSchema.defaultObjectLayers.
|
|
6
|
-
const ObjectLayerSlotSchema = new Schema(
|
|
7
|
-
{
|
|
8
|
-
itemId: { type: String, required: true },
|
|
9
|
-
active: { type: Boolean, default: false },
|
|
10
|
-
quantity: { type: Number, default: 1 },
|
|
11
|
-
},
|
|
12
|
-
{ _id: false },
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
// Per-entity-type simulation defaults. ONLY authoritative item-id wiring
|
|
16
|
-
// lives here — presentation (palette, colour keys) is the client's
|
|
17
|
-
// responsibility and travels through the CyberiaClientHints REST contract.
|
|
18
|
-
const EntityDefaultSchema = new Schema(
|
|
19
|
-
{
|
|
20
|
-
// Entity category string (matches entity_type_str / bot Behavior in game engine)
|
|
21
|
-
entityType: { type: String, required: true },
|
|
22
|
-
// Default ObjectLayer item IDs when the entity is alive and carries no assigned items.
|
|
23
|
-
liveItemIds: { type: [String], default: [] },
|
|
24
|
-
// Default ObjectLayer item IDs for the dead / ghost / respawning state.
|
|
25
|
-
deadItemIds: { type: [String], default: [] },
|
|
26
|
-
// Resource-only inventory items granted on extraction/depletion.
|
|
27
|
-
// These are not auto-activated on the entity itself.
|
|
28
|
-
dropItemIds: { type: [String], default: [] },
|
|
29
|
-
// Full default ObjectLayer inventory for this entity type.
|
|
30
|
-
// Each entry specifies itemId, whether it starts active, and its initial quantity.
|
|
31
|
-
// The coin slot must always have active:false — coins are non-activable.
|
|
32
|
-
// When non-empty this supersedes liveItemIds for inventory initialization.
|
|
33
|
-
defaultObjectLayers: { type: [ObjectLayerSlotSchema], default: [] },
|
|
34
|
-
// Canonical entity behavior (SharedDefaultsCyberia.ENTITY_BEHAVIORS). Empty
|
|
35
|
-
// lets the runtime derive it (armed → hostile, else passive).
|
|
36
|
-
behavior: { type: String, default: '' },
|
|
37
|
-
},
|
|
38
|
-
{ _id: false },
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const SkillConfigEntrySchema = new Schema(
|
|
42
|
-
{
|
|
43
|
-
triggerItemId: { type: String, required: true },
|
|
44
|
-
logicEventIds: { type: [String], default: [] },
|
|
45
|
-
},
|
|
46
|
-
{ _id: false },
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
// ── StatusIconEntrySchema ────────────────────────────────────────────────────
|
|
50
|
-
// Numeric Entity Status Indicator (ESI) IDs. The Go server stamps one of
|
|
51
|
-
// these u8 IDs on every entity in the AOI binary wire format. Visual
|
|
52
|
-
// resolution (icon stem, border colour, bounce) is the C client's job and
|
|
53
|
-
// arrives through the /api/cyberia-client-hints REST contract — NOT here.
|
|
54
17
|
const StatusIconEntrySchema = new Schema(
|
|
55
18
|
{
|
|
56
19
|
id: { type: Number, required: true },
|
|
@@ -61,10 +24,9 @@ const StatusIconEntrySchema = new Schema(
|
|
|
61
24
|
);
|
|
62
25
|
|
|
63
26
|
// ── EconomyRulesSchema ───────────────────────────────────────────────────────
|
|
64
|
-
//
|
|
65
|
-
// cyberia-server-defaults.js
|
|
66
|
-
//
|
|
67
|
-
// See OFF_CHAIN_ECONOMY.md for the full Fountain & Sink architecture.
|
|
27
|
+
// The EconomyRules message shape. Every field defaults from
|
|
28
|
+
// cyberia-server-defaults.js, so a new document is playable at once.
|
|
29
|
+
// See OFF_CHAIN_ECONOMY.md for the Fountain & Sink architecture.
|
|
68
30
|
const EconomyRulesSchema = new Schema(
|
|
69
31
|
{
|
|
70
32
|
// ── Fountains ───────────────────────────────────────────────────────────
|
|
@@ -101,12 +63,12 @@ const EquipmentRulesSchema = new Schema(
|
|
|
101
63
|
|
|
102
64
|
const SkillRulesSchema = new Schema(
|
|
103
65
|
{
|
|
104
|
-
projectileSpawnChance: { type: Number, default: D.skillRules.projectileSpawnChance },
|
|
66
|
+
projectileSpawnChance: { type: Number, default: D.skillRules.projectileSpawnChance, min: 0, max: 1 },
|
|
105
67
|
projectileLifetimeMs: { type: Number, default: D.skillRules.projectileLifetimeMs },
|
|
106
68
|
projectileWidth: { type: Number, default: D.skillRules.projectileWidth },
|
|
107
69
|
projectileHeight: { type: Number, default: D.skillRules.projectileHeight },
|
|
108
70
|
projectileSpeedMultiplier: { type: Number, default: D.skillRules.projectileSpeedMultiplier },
|
|
109
|
-
doppelgangerSpawnChance: { type: Number, default: D.skillRules.doppelgangerSpawnChance },
|
|
71
|
+
doppelgangerSpawnChance: { type: Number, default: D.skillRules.doppelgangerSpawnChance, min: 0, max: 1 },
|
|
110
72
|
doppelgangerLifetimeMs: { type: Number, default: D.skillRules.doppelgangerLifetimeMs },
|
|
111
73
|
doppelgangerSpawnRadius: { type: Number, default: D.skillRules.doppelgangerSpawnRadius },
|
|
112
74
|
doppelgangerInitialLifeFraction: { type: Number, default: D.skillRules.doppelgangerInitialLifeFraction },
|
|
@@ -115,12 +77,11 @@ const SkillRulesSchema = new Schema(
|
|
|
115
77
|
);
|
|
116
78
|
|
|
117
79
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
80
|
+
* Simulation configuration for a Cyberia instance. It is separate from
|
|
81
|
+
* CyberiaInstance so an edit to instance identity or map graph cannot
|
|
82
|
+
* overwrite live simulation parameters.
|
|
121
83
|
*
|
|
122
|
-
* Linked from CyberiaInstance.conf (ObjectId ref).
|
|
123
|
-
* Looked up by instanceCode for CLI / gRPC use.
|
|
84
|
+
* Linked from CyberiaInstance.conf (ObjectId ref), looked up by instanceCode.
|
|
124
85
|
*/
|
|
125
86
|
const CyberiaInstanceConfSchema = new Schema(
|
|
126
87
|
{
|
|
@@ -151,7 +112,7 @@ const CyberiaInstanceConfSchema = new Schema(
|
|
|
151
112
|
playerBaseSpeed: { type: Number, default: D.playerBaseSpeed },
|
|
152
113
|
playerBaseLifeRegenMin: { type: Number, default: D.playerBaseLifeRegenMin },
|
|
153
114
|
playerBaseLifeRegenMax: { type: Number, default: D.playerBaseLifeRegenMax },
|
|
154
|
-
|
|
115
|
+
progressionRules: { type: ProgressionRulesSchema, default: D.progressionRules },
|
|
155
116
|
maxActiveLayers: { type: Number, default: D.maxActiveLayers },
|
|
156
117
|
initialLifeFraction: { type: Number, default: D.initialLifeFraction },
|
|
157
118
|
|
|
@@ -163,15 +124,26 @@ const CyberiaInstanceConfSchema = new Schema(
|
|
|
163
124
|
// Fountain & Sink parameters. See EconomyRulesSchema and OFF_CHAIN_ECONOMY.md.
|
|
164
125
|
economyRules: { type: EconomyRulesSchema },
|
|
165
126
|
|
|
166
|
-
// ──
|
|
167
|
-
lifeRegenChance: { type: Number, default: D.lifeRegenChance },
|
|
168
|
-
maxChance: { type: Number, default: D.maxChance },
|
|
169
|
-
|
|
170
|
-
// ── Entity type
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
127
|
+
// ── Chances, as fractions of 1 ───────────────────────────────────
|
|
128
|
+
lifeRegenChance: { type: Number, default: D.lifeRegenChance, min: 0, max: 1 },
|
|
129
|
+
maxChance: { type: Number, default: D.maxChance, min: 0, max: 1 },
|
|
130
|
+
|
|
131
|
+
// ── Entity type defaults ─────────────────────────────────────────
|
|
132
|
+
// References into the CyberiaEntityTypeDefault collection, which owns the
|
|
133
|
+
// per-entity-type item wiring (live/dead/drop sets, seed inventory, behavior).
|
|
134
|
+
//
|
|
135
|
+
// A reference, never a copy. Embedding the documents here meant the same
|
|
136
|
+
// wiring existed twice — once in the collection, once inside every instance
|
|
137
|
+
// conf — and the two were reconciled by matching item ids, so two instances
|
|
138
|
+
// sharing a skin resolved into each other's defaults on import and export.
|
|
139
|
+
// An id names exactly one document, which is what makes that impossible.
|
|
140
|
+
//
|
|
141
|
+
// An empty list is not "no defaults": it means this instance adds nothing to
|
|
142
|
+
// the canonical ENTITY_TYPE_DEFAULTS, which every world resolves against.
|
|
143
|
+
entityDefaults: {
|
|
144
|
+
type: [{ type: Schema.Types.ObjectId, ref: 'CyberiaEntityTypeDefault' }],
|
|
145
|
+
default: D.entityDefaults,
|
|
146
|
+
},
|
|
175
147
|
|
|
176
148
|
// ── Entity Status Indicators ────────────────────────────────────
|
|
177
149
|
// Overhead icon mapping + per-status border colour.
|
|
@@ -179,9 +151,11 @@ const CyberiaInstanceConfSchema = new Schema(
|
|
|
179
151
|
statusIcons: { type: [StatusIconEntrySchema], default: D.statusIcons },
|
|
180
152
|
|
|
181
153
|
// ── Skill system ─────────────────────────────────────────────────
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
154
|
+
// Which skills an instance runs is not stored: the CyberiaSkill collection owns the
|
|
155
|
+
// definitions, and an instance runs the ones whose trigger item its own content names.
|
|
156
|
+
// See collectInstanceItemIds / selectInstanceSkills (cyberia-instance-items.js). Storing
|
|
157
|
+
// the list here meant maintaining a second answer to that question, and the two disagreed —
|
|
158
|
+
// a trigger a quest asked for exported but never reached the simulation.
|
|
185
159
|
|
|
186
160
|
// Numeric tuning parameters for each skill archetype.
|
|
187
161
|
skillRules: { type: SkillRulesSchema },
|
|
@@ -1,11 +1,83 @@
|
|
|
1
1
|
import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
|
|
2
2
|
import { loggerFactory } from '../../server/ops/logger.js';
|
|
3
3
|
import { DataQuery } from '../../server/storage/data-query.js';
|
|
4
|
+
import {
|
|
5
|
+
CYBERIA_INSTANCE_CONF_DEFAULTS,
|
|
6
|
+
fillInstanceConfDefaults,
|
|
7
|
+
resolveProgressionRules,
|
|
8
|
+
} from '../cyberia-server-defaults/cyberia-server-defaults.js';
|
|
4
9
|
|
|
5
10
|
const logger = loggerFactory(import.meta);
|
|
6
11
|
|
|
12
|
+
const readPath = (object, path) => path.split('.').reduce((node, key) => (node == null ? undefined : node[key]), object);
|
|
13
|
+
|
|
14
|
+
const writePath = (object, path, value) => {
|
|
15
|
+
const keys = path.split('.');
|
|
16
|
+
let node = object;
|
|
17
|
+
for (const key of keys.slice(0, -1)) {
|
|
18
|
+
if (node[key] == null || typeof node[key] !== 'object') node[key] = {};
|
|
19
|
+
node = node[key];
|
|
20
|
+
}
|
|
21
|
+
node[keys.at(-1)] = value;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/* The canonical default for a path, or for the nearest ancestor that has one: a value nested
|
|
25
|
+
* inside an array element has no default of its own, so the whole array falls back. */
|
|
26
|
+
const defaultFor = (path) => {
|
|
27
|
+
const keys = path.split('.');
|
|
28
|
+
for (let depth = keys.length; depth > 0; depth--) {
|
|
29
|
+
const candidate = keys.slice(0, depth).join('.');
|
|
30
|
+
const value = readPath(CYBERIA_INSTANCE_CONF_DEFAULTS, candidate);
|
|
31
|
+
if (value !== undefined) return { path: candidate, value: structuredClone(value) };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const validationPaths = async (Model, conf) => {
|
|
37
|
+
try {
|
|
38
|
+
await new Model(conf).validate();
|
|
39
|
+
return [];
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return Object.keys(error?.errors ?? {});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
7
45
|
class CyberiaInstanceConfService {
|
|
46
|
+
/**
|
|
47
|
+
* Makes a conf whole and valid: fills every missing field, then resets every field the schema
|
|
48
|
+
* rejects to its canonical default.
|
|
49
|
+
*
|
|
50
|
+
* A backup written before a bound tightened, or edited by hand, must still round-trip: the
|
|
51
|
+
* world it describes is worth more than one stale number in it, and resetting that number is
|
|
52
|
+
* what an operator would do by hand. Each reset is logged by path so the decision is visible.
|
|
53
|
+
* A default the schema itself rejects is a code fault, not bad data, and still throws.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} conf - Conf as read from a backup or from the database.
|
|
56
|
+
* @param {import('mongoose').Model} CyberiaInstanceConf - The model whose schema decides.
|
|
57
|
+
* @returns {Promise<{conf: object, resets: Array<{path: string, from: unknown, to: unknown}>}>}
|
|
58
|
+
*/
|
|
59
|
+
static async coerceToSchema(conf, CyberiaInstanceConf) {
|
|
60
|
+
const out = fillInstanceConfDefaults(conf);
|
|
61
|
+
const resets = [];
|
|
62
|
+
// One pass resets the rejected paths; the second proves the defaults themselves pass.
|
|
63
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
64
|
+
const failing = await validationPaths(CyberiaInstanceConf, out);
|
|
65
|
+
if (failing.length === 0) return { conf: out, resets };
|
|
66
|
+
if (pass === 1) throw new Error(`CyberiaInstanceConf defaults fail validation at: ${failing.join(', ')}`);
|
|
67
|
+
for (const path of failing) {
|
|
68
|
+
const fallback = defaultFor(path);
|
|
69
|
+
if (!fallback) throw new Error(`CyberiaInstanceConf has no default to reset "${path}" to`);
|
|
70
|
+
const from = readPath(out, fallback.path);
|
|
71
|
+
writePath(out, fallback.path, fallback.value);
|
|
72
|
+
resets.push({ path: fallback.path, from, to: fallback.value });
|
|
73
|
+
logger.warn(`CyberiaInstanceConf "${fallback.path}" reset to default`, { from, to: fallback.value });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { conf: out, resets };
|
|
77
|
+
}
|
|
78
|
+
|
|
8
79
|
static post = async (req, res, options) => {
|
|
80
|
+
if (Object.hasOwn(req.body, 'progressionRules')) req.body.progressionRules = resolveProgressionRules(req.body.progressionRules);
|
|
9
81
|
/** @type {import('./cyberia-instance-conf.model.js').CyberiaInstanceConfModel} */
|
|
10
82
|
const CyberiaInstanceConf =
|
|
11
83
|
DataBaseProviderService.getModel("CyberiaInstanceConf", options);
|
|
@@ -29,10 +101,11 @@ class CyberiaInstanceConfService {
|
|
|
29
101
|
return { data, total, page, totalPages };
|
|
30
102
|
};
|
|
31
103
|
static put = async (req, res, options) => {
|
|
104
|
+
if (Object.hasOwn(req.body, 'progressionRules')) req.body.progressionRules = resolveProgressionRules(req.body.progressionRules);
|
|
32
105
|
/** @type {import('./cyberia-instance-conf.model.js').CyberiaInstanceConfModel} */
|
|
33
106
|
const CyberiaInstanceConf =
|
|
34
107
|
DataBaseProviderService.getModel("CyberiaInstanceConf", options);
|
|
35
|
-
return await CyberiaInstanceConf.findByIdAndUpdate(req.params.id, req.body);
|
|
108
|
+
return await CyberiaInstanceConf.findByIdAndUpdate(req.params.id, req.body, { runValidators: true });
|
|
36
109
|
};
|
|
37
110
|
static delete = async (req, res, options) => {
|
|
38
111
|
/** @type {import('./cyberia-instance-conf.model.js').CyberiaInstanceConfModel} */
|
|
@@ -28,7 +28,9 @@ class CyberiaMapService {
|
|
|
28
28
|
return { codes };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// Maps are addressed by code, the key the engine navigates by.
|
|
32
|
+
if (req.params.id)
|
|
33
|
+
return await CyberiaMap.findOne(DataQuery.naturalKeyFilter('code', req.params.id)).populate(populateCreator);
|
|
32
34
|
|
|
33
35
|
// Parse query parameters using DataQuery helper
|
|
34
36
|
const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
|
|
@@ -48,6 +50,8 @@ class CyberiaMapService {
|
|
|
48
50
|
if (!map) throw new Error('map not found');
|
|
49
51
|
if (req.auth.user.role !== 'admin' && String(map.creator) !== String(req.auth.user._id))
|
|
50
52
|
throw new Error('insufficient permission');
|
|
53
|
+
const candidate = new CyberiaMap({ ...map.toObject(), ...req.body });
|
|
54
|
+
await candidate.validate();
|
|
51
55
|
const File = DataBaseProviderService.getModel("File", options);
|
|
52
56
|
if (req.body.thumbnail && map.thumbnail && String(req.body.thumbnail) !== String(map.thumbnail)) {
|
|
53
57
|
await File.findByIdAndDelete(map.thumbnail);
|
|
@@ -55,7 +59,7 @@ class CyberiaMapService {
|
|
|
55
59
|
if (req.body.preview && map.preview && String(req.body.preview) !== String(map.preview)) {
|
|
56
60
|
await File.findByIdAndDelete(map.preview);
|
|
57
61
|
}
|
|
58
|
-
return await CyberiaMap.findByIdAndUpdate(req.params.id, req.body, { returnDocument: 'after' });
|
|
62
|
+
return await CyberiaMap.findByIdAndUpdate(req.params.id, req.body, { returnDocument: 'after', runValidators: true });
|
|
59
63
|
};
|
|
60
64
|
static delete = async (req, res, options) => {
|
|
61
65
|
/** @type {import('./cyberia-map.model.js').CyberiaMapModel} */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildCrudController,
|
|
3
|
+
controllerHandler,
|
|
4
|
+
sendSuccess,
|
|
5
|
+
setCrossOriginHeaders,
|
|
6
|
+
} from '../../server/network/middlewares.js';
|
|
7
|
+
import { CyberiaMapAudioConfService } from './cyberia-map-audio-conf.service.js';
|
|
8
|
+
|
|
9
|
+
const context = (options) => ({ host: options.host || 'default', path: options.path || '/' });
|
|
10
|
+
|
|
11
|
+
const CyberiaMapAudioConfController = buildCrudController(CyberiaMapAudioConfService, {
|
|
12
|
+
// Read by map code: the browser client's own path into this API, so it carries the same
|
|
13
|
+
// public-read cross-origin policy the generated CRUD reads do.
|
|
14
|
+
getByMapCode: controllerHandler(
|
|
15
|
+
async (req, res, options) => {
|
|
16
|
+
setCrossOriginHeaders(req, res);
|
|
17
|
+
return sendSuccess(res, await CyberiaMapAudioConfService.getByMapCode(req.params.mapCode, context(options)));
|
|
18
|
+
},
|
|
19
|
+
{ errorStatus: 500 },
|
|
20
|
+
),
|
|
21
|
+
assign: controllerHandler(
|
|
22
|
+
async (req, res, options) =>
|
|
23
|
+
sendSuccess(
|
|
24
|
+
res,
|
|
25
|
+
await CyberiaMapAudioConfService.assign({ ...req.body, mapCode: req.params.mapCode }, context(options)),
|
|
26
|
+
),
|
|
27
|
+
{ errorStatus: 400 },
|
|
28
|
+
),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export { CyberiaMapAudioConfController };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Schema, model } from 'mongoose';
|
|
2
|
+
import { AUDIO_BUSES } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
|
|
3
|
+
|
|
4
|
+
// Omitted fields inherit client or map defaults.
|
|
5
|
+
const AudioSettingsSchema = new Schema(
|
|
6
|
+
{
|
|
7
|
+
bus: { type: String, enum: AUDIO_BUSES },
|
|
8
|
+
volume: { type: Number, min: 0, max: 1 },
|
|
9
|
+
loop: { type: Boolean },
|
|
10
|
+
crossfadeMs: { type: Number, min: 0, max: 10000 },
|
|
11
|
+
pitch: { type: Number, min: 0.25, max: 4 },
|
|
12
|
+
pan: { type: Number, min: 0, max: 1 },
|
|
13
|
+
priority: { type: Number, min: 0, max: 255, validate: Number.isInteger },
|
|
14
|
+
},
|
|
15
|
+
{ _id: false },
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const EventAudioSchema = new Schema(
|
|
19
|
+
{
|
|
20
|
+
logicEventId: { type: String, required: true, trim: true },
|
|
21
|
+
audioCode: { type: String, required: true, trim: true },
|
|
22
|
+
settings: { type: AudioSettingsSchema, default: () => ({}) },
|
|
23
|
+
},
|
|
24
|
+
{ _id: false },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const CyberiaMapAudioConfSchema = new Schema(
|
|
28
|
+
{
|
|
29
|
+
// CyberiaMap.code. One audio configuration per map.
|
|
30
|
+
mapCode: { type: String, required: true, trim: true },
|
|
31
|
+
|
|
32
|
+
// CyberiaAudio.code played while no event binding is active.
|
|
33
|
+
defaultMusic: { type: String, default: '', trim: true },
|
|
34
|
+
|
|
35
|
+
// Logic-event bindings, merged by logicEventId.
|
|
36
|
+
events: { type: [EventAudioSchema], default: [] },
|
|
37
|
+
|
|
38
|
+
// Map-wide defaults. A binding's own settings override these.
|
|
39
|
+
settings: { type: AudioSettingsSchema, default: () => ({}) },
|
|
40
|
+
},
|
|
41
|
+
{ timestamps: true },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CyberiaMapAudioConfSchema.index({ mapCode: 1 }, { unique: true });
|
|
45
|
+
|
|
46
|
+
const CyberiaMapAudioConfModel = model('CyberiaMapAudioConf', CyberiaMapAudioConfSchema);
|
|
47
|
+
|
|
48
|
+
const ProviderSchema = CyberiaMapAudioConfSchema;
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
AudioSettingsSchema,
|
|
52
|
+
CyberiaMapAudioConfModel,
|
|
53
|
+
CyberiaMapAudioConfSchema,
|
|
54
|
+
EventAudioSchema,
|
|
55
|
+
ProviderSchema,
|
|
56
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { registerCrudRoutes } from '../../server/network/middlewares.js';
|
|
3
|
+
import { moderatorGuard } from '../../server/security/auth.js';
|
|
4
|
+
import { CyberiaMapAudioConfController } from './cyberia-map-audio-conf.controller.js';
|
|
5
|
+
|
|
6
|
+
class CyberiaMapAudioConfRouter {
|
|
7
|
+
/**
|
|
8
|
+
* @param {import('../types.js').RouterOptions} options
|
|
9
|
+
* @returns {import('express').Router}
|
|
10
|
+
*/
|
|
11
|
+
static router(options) {
|
|
12
|
+
const router = express.Router();
|
|
13
|
+
|
|
14
|
+
// Map-code routes are registered before the CRUD routes: the generic `/:id`
|
|
15
|
+
// handlers capture every single-segment path once they are mounted.
|
|
16
|
+
router.get(
|
|
17
|
+
'/map-code/:mapCode',
|
|
18
|
+
async (req, res) => await CyberiaMapAudioConfController.getByMapCode(req, res, options),
|
|
19
|
+
);
|
|
20
|
+
router.post(
|
|
21
|
+
'/map-code/:mapCode',
|
|
22
|
+
options.authMiddleware,
|
|
23
|
+
moderatorGuard,
|
|
24
|
+
async (req, res) => await CyberiaMapAudioConfController.assign(req, res, options),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return registerCrudRoutes(router, CyberiaMapAudioConfController, options);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ApiRouter = (options) => CyberiaMapAudioConfRouter.router(options);
|
|
32
|
+
|
|
33
|
+
export { ApiRouter, CyberiaMapAudioConfRouter };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js
|
|
3
|
+
* @namespace CyberiaMapAudioConfServiceServer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
|
|
7
|
+
import { loggerFactory } from '../../server/ops/logger.js';
|
|
8
|
+
import { DataQuery } from '../../server/storage/data-query.js';
|
|
9
|
+
|
|
10
|
+
const logger = loggerFactory(import.meta);
|
|
11
|
+
|
|
12
|
+
// Resolved per call, not per module: reading a configuration must not require the
|
|
13
|
+
// caller to have loaded the cyberia-audio model it never touches.
|
|
14
|
+
const getConfModel = (options) => DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
|
|
15
|
+
const getAudioModel = (options) => DataBaseProviderService.getModel('CyberiaAudio', options);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parses a `<logic-event-id>:<audio-code>` binding as accepted by the CLI's `--set-event` flag.
|
|
19
|
+
* The separator is the first colon, so an audio code may contain one.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} value - Raw flag value.
|
|
22
|
+
* @returns {{logicEventId: string, audioCode: string}} Parsed binding.
|
|
23
|
+
* @throws {Error} When either side of the separator is empty.
|
|
24
|
+
*/
|
|
25
|
+
const parseEventAudioBinding = (value) => {
|
|
26
|
+
const separator = `${value ?? ''}`.indexOf(':');
|
|
27
|
+
const logicEventId = separator === -1 ? '' : `${value}`.slice(0, separator).trim();
|
|
28
|
+
const audioCode = separator === -1 ? '' : `${value}`.slice(separator + 1).trim();
|
|
29
|
+
if (!logicEventId || !audioCode) throw new Error(`Expected <logic-event-id>:<audio-code>, received "${value}"`);
|
|
30
|
+
return { logicEventId, audioCode };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Merges incoming bindings into the ones a map already carries, keyed by logic event.
|
|
35
|
+
*
|
|
36
|
+
* Naming an event replaces that binding and leaves the rest in place, so a CLI call configures
|
|
37
|
+
* what it mentions and nothing else.
|
|
38
|
+
*
|
|
39
|
+
* @param {Array<{logicEventId: string}>} current - Bindings already stored.
|
|
40
|
+
* @param {Array<{logicEventId: string}>} incoming - Bindings being assigned.
|
|
41
|
+
* @returns {Array<object>} The merged list, in stored-then-new order.
|
|
42
|
+
*/
|
|
43
|
+
const mergeEventBindings = (current = [], incoming = []) => {
|
|
44
|
+
const merged = [...current];
|
|
45
|
+
for (const binding of incoming) {
|
|
46
|
+
const index = merged.findIndex((entry) => entry.logicEventId === binding.logicEventId);
|
|
47
|
+
if (index === -1) merged.push(binding);
|
|
48
|
+
else {
|
|
49
|
+
const settings = binding.settings ?? merged[index].settings;
|
|
50
|
+
merged[index] = { ...binding, ...(settings ? { settings } : {}) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return merged;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Accepts a bare asset code or an object carrying one. */
|
|
57
|
+
const readAudioCode = (input) => {
|
|
58
|
+
if (input === null || input === undefined || input === '') return '';
|
|
59
|
+
const code = typeof input === 'string' ? input : input.audioCode ?? input.code;
|
|
60
|
+
if (!code) throw new Error('Missing audio asset code');
|
|
61
|
+
return `${code}`.trim();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
class CyberiaMapAudioConfService {
|
|
65
|
+
/**
|
|
66
|
+
* Asserts that an imported cyberia-audio asset carries this code.
|
|
67
|
+
*
|
|
68
|
+
* The code is the whole reference a binding stores, so this is where referential integrity is
|
|
69
|
+
* enforced: an unimported asset is refused rather than written as a dangling name. Nothing is
|
|
70
|
+
* copied out of the asset — the binding needs no more of it than its identity.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} audioCode - CyberiaAudio.code.
|
|
73
|
+
* @param {{host: string, path: string}} options - Provider context.
|
|
74
|
+
* @returns {Promise<string>} The same code, once it is known to resolve.
|
|
75
|
+
* @throws {Error} When no imported cyberia-audio document carries that code.
|
|
76
|
+
*/
|
|
77
|
+
static assertAudioCode = async (audioCode, options) => {
|
|
78
|
+
const exists = await getAudioModel(options).exists({ code: audioCode });
|
|
79
|
+
if (!exists) {
|
|
80
|
+
throw new Error(`Unknown audio asset "${audioCode}" — import it first with: cyberia audio ${audioCode} --import`);
|
|
81
|
+
}
|
|
82
|
+
return audioCode;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetches the audio configuration of a single map.
|
|
87
|
+
* @param {string} mapCode - CyberiaMap code.
|
|
88
|
+
* @param {{host: string, path: string}} options - Provider context.
|
|
89
|
+
* @returns {Promise<object|null>} Configuration document, or null when the map has none.
|
|
90
|
+
*/
|
|
91
|
+
static getByMapCode = async (mapCode, options) => {
|
|
92
|
+
return await getConfModel(options).findOne({ mapCode }).lean();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Bulk-assigns map audio rules, upserting the map's configuration document.
|
|
97
|
+
*
|
|
98
|
+
* Two callers with two intents share this: the CLI amends one binding at a time and must leave
|
|
99
|
+
* the others alone, while a seed states the whole configuration and must converge on it —
|
|
100
|
+
* otherwise a binding whose logic event was renamed or dropped lives on forever, and the client
|
|
101
|
+
* keeps asking for an asset nothing declares any more. `replaceEvents` is which of the two
|
|
102
|
+
* this call is.
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} rules - Assignment payload.
|
|
105
|
+
* @param {string} rules.mapCode - Target CyberiaMap code.
|
|
106
|
+
* @param {string|null} [rules.defaultMusic] - Default music asset code, or '' / null to clear it.
|
|
107
|
+
* @param {Array<{logicEventId: string, audioCode: string, settings?: object}>} [rules.events] - Bindings.
|
|
108
|
+
* @param {boolean} [rules.replaceEvents=false] - Make `events` the map's complete binding set.
|
|
109
|
+
* @param {object} [rules.settings] - Map-wide volume/loop/crossfade defaults.
|
|
110
|
+
* @param {{host: string, path: string}} options - Provider context.
|
|
111
|
+
* @returns {Promise<object>} The updated configuration document.
|
|
112
|
+
*/
|
|
113
|
+
static assign = async ({ mapCode, defaultMusic, events = [], replaceEvents = false, settings }, options) => {
|
|
114
|
+
if (!mapCode) throw new Error('mapCode is required to assign map audio rules');
|
|
115
|
+
const CyberiaMapAudioConf = getConfModel(options);
|
|
116
|
+
|
|
117
|
+
const bindings = await Promise.all(
|
|
118
|
+
events.map(async ({ logicEventId, settings: entrySettings, ...reference }) => {
|
|
119
|
+
if (!logicEventId) throw new Error(`Missing logicEventId for an audio binding on map "${mapCode}"`);
|
|
120
|
+
return {
|
|
121
|
+
logicEventId: `${logicEventId}`.trim(),
|
|
122
|
+
audioCode: await CyberiaMapAudioConfService.assertAudioCode(readAudioCode(reference), options),
|
|
123
|
+
...(entrySettings ? { settings: entrySettings } : {}),
|
|
124
|
+
};
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const defaultMusicCode = defaultMusic === undefined ? undefined : readAudioCode(defaultMusic);
|
|
129
|
+
if (defaultMusicCode) await CyberiaMapAudioConfService.assertAudioCode(defaultMusicCode, options);
|
|
130
|
+
|
|
131
|
+
const conf = (await CyberiaMapAudioConf.findOne({ mapCode })) || new CyberiaMapAudioConf({ mapCode });
|
|
132
|
+
|
|
133
|
+
if (defaultMusicCode !== undefined) conf.defaultMusic = defaultMusicCode;
|
|
134
|
+
if (settings) conf.settings = { ...(conf.settings?.toObject?.() ?? conf.settings ?? {}), ...settings };
|
|
135
|
+
|
|
136
|
+
const dropped = replaceEvents
|
|
137
|
+
? (conf.events ?? [])
|
|
138
|
+
.map(({ logicEventId }) => logicEventId)
|
|
139
|
+
.filter((logicEventId) => !bindings.some((binding) => binding.logicEventId === logicEventId))
|
|
140
|
+
: [];
|
|
141
|
+
if (replaceEvents) conf.events = bindings;
|
|
142
|
+
else if (bindings.length > 0) conf.events = mergeEventBindings(conf.events, bindings);
|
|
143
|
+
|
|
144
|
+
await conf.save();
|
|
145
|
+
logger.info(`Assigned map audio rules for "${mapCode}"`, {
|
|
146
|
+
defaultMusic: conf.defaultMusic || null,
|
|
147
|
+
events: conf.events.length,
|
|
148
|
+
...(dropped.length > 0 ? { dropped } : {}),
|
|
149
|
+
});
|
|
150
|
+
return conf;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
static post = async (req, res, options) => {
|
|
154
|
+
/** @type {import('./cyberia-map-audio-conf.model.js').CyberiaMapAudioConfModel} */
|
|
155
|
+
const CyberiaMapAudioConf = DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
|
|
156
|
+
return await new CyberiaMapAudioConf(req.body).save();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
static get = async (req, res, options) => {
|
|
160
|
+
/** @type {import('./cyberia-map-audio-conf.model.js').CyberiaMapAudioConfModel} */
|
|
161
|
+
const CyberiaMapAudioConf = DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
|
|
162
|
+
if (req.params.id) return await CyberiaMapAudioConf.findById(req.params.id);
|
|
163
|
+
|
|
164
|
+
const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
|
|
165
|
+
|
|
166
|
+
const [data, total] = await Promise.all([
|
|
167
|
+
CyberiaMapAudioConf.find(query).sort(sort).limit(limit).skip(skip),
|
|
168
|
+
CyberiaMapAudioConf.countDocuments(query),
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
const totalPages = Math.ceil(total / limit);
|
|
172
|
+
return { data, total, page, totalPages };
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
static put = async (req, res, options) => {
|
|
176
|
+
/** @type {import('./cyberia-map-audio-conf.model.js').CyberiaMapAudioConfModel} */
|
|
177
|
+
const CyberiaMapAudioConf = DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
|
|
178
|
+
return await CyberiaMapAudioConf.findByIdAndUpdate(req.params.id, req.body);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
static delete = async (req, res, options) => {
|
|
182
|
+
/** @type {import('./cyberia-map-audio-conf.model.js').CyberiaMapAudioConfModel} */
|
|
183
|
+
const CyberiaMapAudioConf = DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
|
|
184
|
+
if (req.params.id) return await CyberiaMapAudioConf.findByIdAndDelete(req.params.id);
|
|
185
|
+
else return await CyberiaMapAudioConf.deleteMany();
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { CyberiaMapAudioConfService, mergeEventBindings, parseEventAudioBinding };
|
|
@@ -9,10 +9,10 @@ class CyberiaQuestRouter {
|
|
|
9
9
|
*/
|
|
10
10
|
static router(options) {
|
|
11
11
|
const router = express.Router();
|
|
12
|
-
// Direct lookup
|
|
12
|
+
// Direct lookup of quest metadata by code.
|
|
13
13
|
router.get(`/code/:code`, async (req, res) => await CyberiaQuestController.getByCode(req, res, options));
|
|
14
|
-
// Quest offers
|
|
15
|
-
//
|
|
14
|
+
// Quest offers by binding cell. The client resolves its Quest tab from the
|
|
15
|
+
// interacted entity's cell, with no CyberiaAction lookup.
|
|
16
16
|
router.get(
|
|
17
17
|
`/cell/:mapCode/:cellX/:cellY`,
|
|
18
18
|
async (req, res) => await CyberiaQuestController.getByCell(req, res, options),
|
|
@@ -50,9 +50,9 @@ class CyberiaQuestService {
|
|
|
50
50
|
if (!code) throw new Error('code parameter is required');
|
|
51
51
|
const data = await CyberiaQuest.findOne({ code }).lean();
|
|
52
52
|
if (data) return data;
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
53
|
+
// The fallback world serves quests from the canonical defaults and never
|
|
54
|
+
// persists them. Serve the same defaults, so the client resolves metadata
|
|
55
|
+
// with no seeded quest in Mongo.
|
|
56
56
|
const fallback = DefaultCyberiaQuests.find((q) => q.code === code);
|
|
57
57
|
if (fallback) return fallback;
|
|
58
58
|
throw new Error(`No quest found for code: ${code}`);
|
|
@@ -4,7 +4,7 @@ import { Schema, model } from 'mongoose';
|
|
|
4
4
|
|
|
5
5
|
const CyberiaQuestProgressSchema = new Schema(
|
|
6
6
|
{
|
|
7
|
-
// Matches the
|
|
7
|
+
// Matches the simulation server's player UUID
|
|
8
8
|
playerId: { type: String, required: true, trim: true },
|
|
9
9
|
// References CyberiaQuest.code
|
|
10
10
|
questCode: { type: String, required: true, trim: true },
|