@volter/editor-game 0.5.65
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/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OID-SOURCE persistence — the {@link SourcePersistenceBackend} backend for a live
|
|
3
|
+
* three world whose OWN source the serve-time OID transform reached.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT EXISTS, measured rather than assumed. `createCreationSitePersistence`
|
|
6
|
+
* anchors an edit at the `new` expression that constructed the object, read
|
|
7
|
+
* from the creation-site registry (`creation-site-registry.ts`). A vendored R3F
|
|
8
|
+
* game has none: every `THREE.Mesh`/`THREE.PointLight` in it is constructed by
|
|
9
|
+
* react-three-fiber's reconciler inside `node_modules`, so the registry answers
|
|
10
|
+
* `constructed outside project source` for EVERY object in the world and the
|
|
11
|
+
* creation-site writer can never fire once. The game's own source still says
|
|
12
|
+
* exactly where each object came from — it says it in JSX
|
|
13
|
+
* (`<mesh position={[0, 1, 0]}>`), and `vite-plugin-ui-oid.ts` already stamps
|
|
14
|
+
* that callsite onto the constructed object as `userData.oid`. This backend is
|
|
15
|
+
* the other end of that stamp.
|
|
16
|
+
*
|
|
17
|
+
* THE WRITE LANE IS THE NATIVE ONE, not a second implementation of it: a write
|
|
18
|
+
* is `SourceWriteBackend.writeProp(oid, prop, value)` — the same
|
|
19
|
+
* `/__ui-source/prop` endpoint the first-party R3F adapter's gizmo commits
|
|
20
|
+
* through (`r3f-source-authoring-adapter.ts`), so the JSX planning, the
|
|
21
|
+
* expression-bound refusal, the checksum guard and the vendored-lock recorder
|
|
22
|
+
* (`writeEditableSource`) are literally the same code for a vendored game as
|
|
23
|
+
* for a project's own world. Wrapped in `withProjectSourceHistory`, so one
|
|
24
|
+
* gesture is one checksum-guarded, undoable project-history transaction.
|
|
25
|
+
*
|
|
26
|
+
* NO LIVE-HALF HISTORY RESOURCE, and that is a fact about R3F rather than an
|
|
27
|
+
* omission. `IngestSourcePersistence.ensureLiveResource` exists because an
|
|
28
|
+
* unmodified imperative game does not re-derive its scene from source —
|
|
29
|
+
* nothing re-runs `#setupLights()` because a literal changed — so undoing the
|
|
30
|
+
* file alone would leave the running game showing the edit it just undid. A
|
|
31
|
+
* fiber world DOES re-derive: restoring the file re-renders the component and
|
|
32
|
+
* the object's props come back from source, which is the same reason the
|
|
33
|
+
* first-party R3F lane needs no live half either. Where there is no HMR at all
|
|
34
|
+
* (a dev server whose watcher ignores the tree), the file is still truth and
|
|
35
|
+
* the next mount shows it.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { reportIngestSourceRefusal } from '../../host/authoring/ingest-source-persistence';
|
|
39
|
+
import type { SourcePersistenceBackend } from '../../host/authoring/source-persistence-backend';
|
|
40
|
+
import { LIVE_ONLY_DESTINATION } from '@volter/editor-core/authoring/write-pipe';
|
|
41
|
+
import type { ChannelValue } from '@volter/editor-core/creation-site-edit';
|
|
42
|
+
import { channelFor } from '@volter/editor-core/creation-site-edit';
|
|
43
|
+
import { editorConsole } from '@volter/editor-core/editor-console';
|
|
44
|
+
import { editorIsAuthoring } from '@volter/editor-core/editor-session-mode';
|
|
45
|
+
import type { HistoryService } from '@volter/editor-core/history/history-service';
|
|
46
|
+
import { withProjectSourceHistory } from '@volter/editor-core/history/source-history-backend';
|
|
47
|
+
import { getCurrentProject } from '@volter/editor-core/project-manager';
|
|
48
|
+
import type { OidEntry } from '@volter/editor-core/ui-source/oid-transform';
|
|
49
|
+
import { bodyPlacedChannel, physicsRefusal } from '@volter/editor-core/ui-source/r3f-physics-binding';
|
|
50
|
+
import type { SourceWriteBackend } from '@volter/editor-core/ui-source/source-write-backend';
|
|
51
|
+
import { sourceWriteBackendIfPrimed } from '@volter/editor-core/ui-source/tier-source-write-backend';
|
|
52
|
+
import type { NodeCreationSite, WriteAck } from '@volter/editor-project/adapter';
|
|
53
|
+
|
|
54
|
+
/** What `AuthoringAdapter.persistence.destination` reports once this backend is
|
|
55
|
+
* actually able to write. */
|
|
56
|
+
export const OID_SOURCE_DESTINATION = "the game's own JSX source (source-stamp write-back)";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The editor property paths this backend can express as a JSX attribute, and
|
|
60
|
+
* the attribute each one IS.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately a subset of `CREATION_SITE_CHANNELS` rather than a parallel
|
|
63
|
+
* table: a path absent here is refused BY NAME (`gate` says which), never
|
|
64
|
+
* guessed at. `material.color` is the notable absence — its owner hop lands on
|
|
65
|
+
* the material, and a material's own JSX tag (`<meshStandardMaterial color=…>`)
|
|
66
|
+
* carries its own stamp, so the adapter hands us that oid and `color` is the
|
|
67
|
+
* right attribute on it. `name`/`visible`/shadow flags are ordinary R3F props
|
|
68
|
+
* on the object's own tag.
|
|
69
|
+
*/
|
|
70
|
+
const JSX_PROP_BY_CHANNEL: Readonly<Record<string, string>> = {
|
|
71
|
+
position: 'position',
|
|
72
|
+
rotation: 'rotation',
|
|
73
|
+
scale: 'scale',
|
|
74
|
+
visible: 'visible',
|
|
75
|
+
name: 'name',
|
|
76
|
+
'material.color': 'color',
|
|
77
|
+
'light.color': 'color',
|
|
78
|
+
'light.intensity': 'intensity',
|
|
79
|
+
'light.distance': 'distance',
|
|
80
|
+
'camera.fov': 'fov',
|
|
81
|
+
'camera.near': 'near',
|
|
82
|
+
'camera.far': 'far',
|
|
83
|
+
'shadow.cast': 'castShadow',
|
|
84
|
+
'shadow.receive': 'receiveShadow',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** ≤4 decimals, no trailing zeros — the same notation the R3F source adapter
|
|
88
|
+
* writes tuples in, so a hand-written file and a gizmo-written one read alike. */
|
|
89
|
+
function fmt(n: number): string {
|
|
90
|
+
const rounded = Math.round(n * 1e4) / 1e4;
|
|
91
|
+
return Object.is(rounded, -0) ? '0' : String(rounded);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One channel value as the RAW value text `writeProp` takes — `[0, 26, 20]`,
|
|
96
|
+
* `26`, `false`, `#ff8800`.
|
|
97
|
+
*
|
|
98
|
+
* NOT the attribute text. The writer decides the wrapper itself
|
|
99
|
+
* (`ui-source/writer.ts`'s `formatPropReplacement`/`formatNewAttr`): it matches
|
|
100
|
+
* the shape of the literal already in source, and a value arriving pre-wrapped
|
|
101
|
+
* in `{…}` is a SHAPE MISMATCH it silently refuses — `{changed:false}` with no
|
|
102
|
+
* error, which is the quietest possible failure and is exactly what this got
|
|
103
|
+
* wrong first (measured live: the gesture reached the server and reported
|
|
104
|
+
* "the source writer made no change"). The first-party R3F lane's
|
|
105
|
+
* `commitTransformEdit` sends the same raw form.
|
|
106
|
+
*
|
|
107
|
+
* `null` ⇒ this backend cannot express the value, which is a refusal with a
|
|
108
|
+
* reason rather than a best effort.
|
|
109
|
+
*/
|
|
110
|
+
export function jsxPropValueText(property: string, value: ChannelValue): string | null {
|
|
111
|
+
const kind = channelFor(property)?.kind;
|
|
112
|
+
if (kind === 'vector3' || kind === 'euler') {
|
|
113
|
+
if (!Array.isArray(value) || value.length !== 3) return null;
|
|
114
|
+
if (!value.every((n) => typeof n === 'number' && Number.isFinite(n))) return null;
|
|
115
|
+
return `[${(value as number[]).map(fmt).join(', ')}]`;
|
|
116
|
+
}
|
|
117
|
+
if (kind === 'number') {
|
|
118
|
+
return typeof value === 'number' && Number.isFinite(value) ? fmt(value) : null;
|
|
119
|
+
}
|
|
120
|
+
if (kind === 'boolean') return typeof value === 'boolean' ? String(value) : null;
|
|
121
|
+
if (kind === 'color' || kind === 'string') {
|
|
122
|
+
return typeof value === 'string' ? value.replace(/["\\]/g, '') : null;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Why a component callsite's OWN transform prop would be ignored if written —
|
|
129
|
+
* `null` when there is no reason, which is the ordinary case.
|
|
130
|
+
*
|
|
131
|
+
* The index carries the component's source-proven authoring contract, so this
|
|
132
|
+
* is read from an analysis of the definition rather than guessed: a component
|
|
133
|
+
* that neither accepts nor spreads the prop would take the write silently and
|
|
134
|
+
* render exactly as before, which is source that lies. The first-party R3F lane
|
|
135
|
+
* makes the same two refusals from the same field
|
|
136
|
+
* (`r3f-source-authoring-adapter.ts`'s `transformEditability`).
|
|
137
|
+
*/
|
|
138
|
+
function contractRefusal(entry: OidEntry | null, prop: string): string | null {
|
|
139
|
+
// A body binding outranks the contract question: when a simulation owns this
|
|
140
|
+
// element's transform, "does not forward position" is not merely unhelpful,
|
|
141
|
+
// it is about the wrong artifact (`ui-source/r3f-physics-binding.ts`).
|
|
142
|
+
const physics = physicsRefusal(entry ?? undefined, prop);
|
|
143
|
+
if (physics) return physics;
|
|
144
|
+
const contract = entry?.r3fAuthoring;
|
|
145
|
+
if (!contract) return null;
|
|
146
|
+
if (prop !== 'position' && prop !== 'rotation' && prop !== 'scale') return null;
|
|
147
|
+
const tag = `<${entry?.tag ?? 'this component'}>`;
|
|
148
|
+
if (contract.simulationOwnedTransform === true) {
|
|
149
|
+
return (
|
|
150
|
+
`${tag} drives its own ${prop} every frame — this instance is placed by the simulation, ` +
|
|
151
|
+
'not by the editor'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (!(contract.transformProps as readonly string[]).includes(prop)) {
|
|
155
|
+
return `${tag} does not forward ${prop} to its native root`;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface OidTransformSourceCommitter {
|
|
161
|
+
availability(sourceOid: string | undefined): { available: boolean; reason?: string };
|
|
162
|
+
commit(
|
|
163
|
+
sourceOid: string | undefined,
|
|
164
|
+
values: Readonly<Record<string, ChannelValue>>,
|
|
165
|
+
): Promise<WriteAck>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function oidSourceAnchor(
|
|
169
|
+
entry: OidEntry,
|
|
170
|
+
): Extract<NodeCreationSite, { anchored: true; kind: 'source' }> {
|
|
171
|
+
const normalized = entry.file.replaceAll('\\', '/');
|
|
172
|
+
const root = getCurrentProject()?.rootPath.replaceAll('\\', '/').replace(/\/$/, '');
|
|
173
|
+
const file =
|
|
174
|
+
root && normalized.startsWith(`${root}/`) ? normalized.slice(root.length + 1) : normalized;
|
|
175
|
+
return {
|
|
176
|
+
anchored: true,
|
|
177
|
+
kind: 'source',
|
|
178
|
+
file,
|
|
179
|
+
line: entry.line,
|
|
180
|
+
col: entry.col,
|
|
181
|
+
display: `${file}:${entry.line}`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Read source locations without granting persistence to a running world. */
|
|
186
|
+
export async function readOidSourceAnchors(
|
|
187
|
+
backend?: SourceWriteBackend,
|
|
188
|
+
): Promise<(sourceOid: string) => NodeCreationSite | null> {
|
|
189
|
+
try {
|
|
190
|
+
const source = backend ?? sourceWriteBackendIfPrimed('Live source locations');
|
|
191
|
+
const index = await source?.index?.();
|
|
192
|
+
return (sourceOid) => {
|
|
193
|
+
const entry = index?.[sourceOid];
|
|
194
|
+
return entry ? oidSourceAnchor(entry) : null;
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
// Missing provenance must not prevent the game from starting.
|
|
198
|
+
return () => null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function writeTransformLiterals(
|
|
203
|
+
backend: SourceWriteBackend,
|
|
204
|
+
sourceOid: string,
|
|
205
|
+
writes: readonly { property: string; text: string }[],
|
|
206
|
+
): Promise<number> {
|
|
207
|
+
const writeProp = backend.writeProp;
|
|
208
|
+
if (!writeProp) throw new Error('the gesture-scoped source writer cannot write JSX props');
|
|
209
|
+
let changed = 0;
|
|
210
|
+
for (const { property, text } of writes) {
|
|
211
|
+
const result = await writeProp(sourceOid, property, text, {
|
|
212
|
+
addIfMissing: true,
|
|
213
|
+
allowShapeUpgrade: true,
|
|
214
|
+
});
|
|
215
|
+
if (result.dynamic) {
|
|
216
|
+
throw new Error(`${property} is bound to a JSX expression, not an authored literal`);
|
|
217
|
+
}
|
|
218
|
+
if (!result.changed && result.error) throw new Error(result.error);
|
|
219
|
+
if (result.changed) changed++;
|
|
220
|
+
}
|
|
221
|
+
return changed;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The explicit Play→source door. It is deliberately separate from
|
|
225
|
+
* createOidSourcePersistence: that backend's ordinary gesture gate refuses
|
|
226
|
+
* Play, while this one is invoked only after the user explicitly attributes a
|
|
227
|
+
* selected live pose to source. Both still use the exact same OID writer and
|
|
228
|
+
* project-history transaction. */
|
|
229
|
+
export function createOidTransformSourceCommitter(
|
|
230
|
+
options: OidSourcePersistenceOptions,
|
|
231
|
+
): OidTransformSourceCommitter {
|
|
232
|
+
const raw = options.backend ?? sourceWriteBackendIfPrimed('The Play→source transform committer');
|
|
233
|
+
const backend = withProjectSourceHistory(raw, options.history);
|
|
234
|
+
const availability = (sourceOid: string | undefined): { available: boolean; reason?: string } => {
|
|
235
|
+
if (!sourceOid) {
|
|
236
|
+
return {
|
|
237
|
+
available: false,
|
|
238
|
+
reason: 'This live node has no OID source stamp, so no authored literal owns its pose.',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (!backend?.runGesture || !backend.writeProp || !backend.index) {
|
|
242
|
+
return {
|
|
243
|
+
available: false,
|
|
244
|
+
reason: 'This editor session cannot open a checksum-guarded project-source gesture.',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return { available: true };
|
|
248
|
+
};
|
|
249
|
+
const refuse = (reason: string): WriteAck => {
|
|
250
|
+
editorConsole.warn(`[play] Commit live transform to source refused: ${reason}`, 'play-mode');
|
|
251
|
+
return { destination: LIVE_ONLY_DESTINATION, persisted: false };
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
availability,
|
|
256
|
+
async commit(sourceOid, values) {
|
|
257
|
+
const available = availability(sourceOid);
|
|
258
|
+
if (!available.available || !sourceOid || !backend?.runGesture || !backend.index) {
|
|
259
|
+
return refuse(available.reason ?? 'the source writer is unavailable');
|
|
260
|
+
}
|
|
261
|
+
const index = await backend.index();
|
|
262
|
+
const entry = index[sourceOid] ?? null;
|
|
263
|
+
if (!entry) return refuse(`the current source index has no entry for OID "${sourceOid}"`);
|
|
264
|
+
const writes = ['position', 'rotation', 'scale'].map((property) => {
|
|
265
|
+
const blocked = contractRefusal(entry, property);
|
|
266
|
+
if (blocked) throw new Error(blocked);
|
|
267
|
+
const value = values[property];
|
|
268
|
+
const text = value === undefined ? null : jsxPropValueText(property, value);
|
|
269
|
+
if (text === null) throw new Error(`${property} is not a finite transform literal`);
|
|
270
|
+
return { property, text };
|
|
271
|
+
});
|
|
272
|
+
let changed: number;
|
|
273
|
+
try {
|
|
274
|
+
changed = await backend.runGesture('Commit Play Transform to Source', (scoped) =>
|
|
275
|
+
writeTransformLiterals(scoped, sourceOid, writes),
|
|
276
|
+
);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
279
|
+
}
|
|
280
|
+
if (changed === 0) {
|
|
281
|
+
editorConsole.log(
|
|
282
|
+
'[play] Source already matches the selected live transform.',
|
|
283
|
+
'play-mode',
|
|
284
|
+
);
|
|
285
|
+
return { destination: 'source already matched the live transform', persisted: false };
|
|
286
|
+
}
|
|
287
|
+
editorConsole.log(
|
|
288
|
+
`[play] Committed ${changed} live transform literal(s) to ${entry.file}:${entry.line}.`,
|
|
289
|
+
'play-mode',
|
|
290
|
+
);
|
|
291
|
+
return { destination: OID_SOURCE_DESTINATION, persisted: true };
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface OidSourcePersistenceOptions {
|
|
297
|
+
readonly history: HistoryService | null;
|
|
298
|
+
/**
|
|
299
|
+
* Injectable so a unit test can drive the whole gesture without a server.
|
|
300
|
+
* Absent in production: the backend builds the ordinary HTTP one when THIS
|
|
301
|
+
* SESSION'S HOST serves the `/__ui-source/*` recorder, and stays at the
|
|
302
|
+
* honest live-only floor when it does not. See
|
|
303
|
+
* `../ui-source/tier-source-write-backend.ts`.
|
|
304
|
+
*/
|
|
305
|
+
readonly backend?: SourceWriteBackend | undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function createOidSourcePersistence(
|
|
309
|
+
options: OidSourcePersistenceOptions,
|
|
310
|
+
): SourcePersistenceBackend {
|
|
311
|
+
let backend: SourceWriteBackend | null = null;
|
|
312
|
+
/** oid → where the served source says that element is. Fetched once per
|
|
313
|
+
* attach and refreshed after every accepted write, because a write moves
|
|
314
|
+
* the lines below it. */
|
|
315
|
+
let index = new Map<string, OidEntry>();
|
|
316
|
+
let indexLoaded = false;
|
|
317
|
+
|
|
318
|
+
const refreshIndex = async (): Promise<void> => {
|
|
319
|
+
const raw = backend?.index;
|
|
320
|
+
if (!raw) return;
|
|
321
|
+
try {
|
|
322
|
+
index = new Map(Object.entries(await raw.call(backend)));
|
|
323
|
+
indexLoaded = true;
|
|
324
|
+
} catch {
|
|
325
|
+
// Honest degradation: without the index this backend still WRITES (the
|
|
326
|
+
// server resolves the oid itself); it just cannot name the file:line in
|
|
327
|
+
// the sentence it shows beforehand.
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const entryOf = (sourceOid: string | undefined): OidEntry | null =>
|
|
332
|
+
sourceOid ? (index.get(sourceOid) ?? null) : null;
|
|
333
|
+
|
|
334
|
+
const displayOf = (sourceOid: string | undefined): string | null => {
|
|
335
|
+
const entry = entryOf(sourceOid);
|
|
336
|
+
return entry ? oidSourceAnchor(entry).display : null;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The cheap gate, read from cached state only — the live-only path must stay
|
|
341
|
+
* synchronous (see `SourcePersistenceBackend.gate`).
|
|
342
|
+
*
|
|
343
|
+
* The literal-vs-expression question is deliberately NOT asked here: it is
|
|
344
|
+
* answered against the game's real bytes by the same server planner the
|
|
345
|
+
* first-party R3F lane uses, which comes back `dynamic: true` and is reported
|
|
346
|
+
* as the refusal it is. Guessing at it from a stale client-side copy of the
|
|
347
|
+
* source is how two condition lists start disagreeing.
|
|
348
|
+
*/
|
|
349
|
+
const verdict = (
|
|
350
|
+
sourceOid: string | undefined,
|
|
351
|
+
property: string,
|
|
352
|
+
): { ok: true } | { ok: false; reason: string } => {
|
|
353
|
+
if (!sourceOid) {
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
reason:
|
|
357
|
+
'this object carries no source stamp — the game built it at runtime rather than ' +
|
|
358
|
+
'writing it as an element of its own source',
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (!backend?.writeProp) {
|
|
362
|
+
return { ok: false, reason: 'this editor tier cannot write project source' };
|
|
363
|
+
}
|
|
364
|
+
const prop = JSX_PROP_BY_CHANNEL[property];
|
|
365
|
+
if (!prop) {
|
|
366
|
+
return { ok: false, reason: `${property} has no JSX attribute this seam can write` };
|
|
367
|
+
}
|
|
368
|
+
const ignored = contractRefusal(entryOf(sourceOid), prop);
|
|
369
|
+
if (ignored) return { ok: false, reason: ignored };
|
|
370
|
+
// The MODE, last: everything above is a fact about this element that no
|
|
371
|
+
// amount of holding the game would change.
|
|
372
|
+
if (!editorIsAuthoring()) {
|
|
373
|
+
const at = displayOf(sourceOid);
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
reason: `${at ? `written at ${at}` : 'source-stamped'}, but this game is playing — hold it to author`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return { ok: true };
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
attach() {
|
|
384
|
+
const raw =
|
|
385
|
+
options.backend ??
|
|
386
|
+
sourceWriteBackendIfPrimed('This world’s OID source persistence') ??
|
|
387
|
+
null;
|
|
388
|
+
backend = raw ? (withProjectSourceHistory(raw, options.history) ?? raw) : null;
|
|
389
|
+
void refreshIndex();
|
|
390
|
+
},
|
|
391
|
+
anchor(sourceOid) {
|
|
392
|
+
const entry = index.get(sourceOid);
|
|
393
|
+
if (!entry) return null;
|
|
394
|
+
return oidSourceAnchor(entry);
|
|
395
|
+
},
|
|
396
|
+
physicsPlaced(sourceOid, property) {
|
|
397
|
+
// `r3f-physics-binding.ts` owns this derivation, and the first-party R3F
|
|
398
|
+
// lane's own classifier reads the same function — two copies of "does a
|
|
399
|
+
// body read this value?" is how two lanes come to disagree about one
|
|
400
|
+
// element.
|
|
401
|
+
return bodyPlacedChannel(
|
|
402
|
+
entryOf(sourceOid) ?? undefined,
|
|
403
|
+
JSX_PROP_BY_CHANNEL[property] ?? property,
|
|
404
|
+
);
|
|
405
|
+
},
|
|
406
|
+
describe({ sourceOid, property }) {
|
|
407
|
+
const decided = verdict(sourceOid, property);
|
|
408
|
+
if (!decided.ok) return `Live-only edit — ${decided.reason}.`;
|
|
409
|
+
const at = displayOf(sourceOid);
|
|
410
|
+
return at
|
|
411
|
+
? `Persisting to ${at}.`
|
|
412
|
+
: `Persisting to this element's own JSX${indexLoaded ? '' : ' (source index still loading)'}.`;
|
|
413
|
+
},
|
|
414
|
+
gate({ sourceOid, property }) {
|
|
415
|
+
return verdict(sourceOid, property);
|
|
416
|
+
},
|
|
417
|
+
armed: () => backend?.writeProp !== undefined && editorIsAuthoring(),
|
|
418
|
+
report: (label, reason) => reportIngestSourceRefusal(label, reason),
|
|
419
|
+
async write(request) {
|
|
420
|
+
const decided = verdict(request.sourceOid, request.property);
|
|
421
|
+
if (!decided.ok) {
|
|
422
|
+
reportIngestSourceRefusal(request.label, decided.reason);
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
const prop = JSX_PROP_BY_CHANNEL[request.property]!;
|
|
426
|
+
const text = jsxPropValueText(request.property, request.next);
|
|
427
|
+
if (text === null) {
|
|
428
|
+
reportIngestSourceRefusal(
|
|
429
|
+
request.label,
|
|
430
|
+
`the new ${request.property} is not a value this seam can spell as a JSX literal`,
|
|
431
|
+
);
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const result = await backend!.writeProp!(request.sourceOid!, prop, text, {
|
|
436
|
+
addIfMissing: true,
|
|
437
|
+
allowShapeUpgrade: true,
|
|
438
|
+
});
|
|
439
|
+
if (result.dynamic) {
|
|
440
|
+
reportIngestSourceRefusal(
|
|
441
|
+
request.label,
|
|
442
|
+
`${prop} at ${displayOf(request.sourceOid) ?? 'this element'} is bound to a JSX expression, not a literal`,
|
|
443
|
+
);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
if (!result.changed) {
|
|
447
|
+
reportIngestSourceRefusal(
|
|
448
|
+
request.label,
|
|
449
|
+
result.error ?? 'the source writer made no change',
|
|
450
|
+
);
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
editorConsole.log(
|
|
454
|
+
`[ingest] ${request.label} written to the game's own source — ${prop} → ${
|
|
455
|
+
displayOf(request.sourceOid) ?? request.sourceOid
|
|
456
|
+
}`,
|
|
457
|
+
);
|
|
458
|
+
// The write moved the lines below it; every later `describe` reads this
|
|
459
|
+
// index, so a stale copy would start naming the wrong line.
|
|
460
|
+
void refreshIndex();
|
|
461
|
+
return true;
|
|
462
|
+
} catch (error) {
|
|
463
|
+
reportIngestSourceRefusal(
|
|
464
|
+
request.label,
|
|
465
|
+
`the write failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
466
|
+
);
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
/**
|
|
471
|
+
* THE REMOVAL DOOR, on the SAME native lane as the write: the
|
|
472
|
+
* `/__ui-source/prop` endpoint's `value: null` sentinel
|
|
473
|
+
* (`SourceWriteBackend.removeProp`), which the middleware routes to
|
|
474
|
+
* `removePropAttribute` and writes through `writeEditableSource` — so a
|
|
475
|
+
* removal on a repo-vendored game records in that game's lock in the same
|
|
476
|
+
* gesture a value write does. Nothing here is a second implementation of
|
|
477
|
+
* anything; the recorder, the checksum guard and the project-history
|
|
478
|
+
* transaction (`withProjectSourceHistory` wraps `removeProp` too) are
|
|
479
|
+
* literally the same code.
|
|
480
|
+
*/
|
|
481
|
+
removal: {
|
|
482
|
+
/**
|
|
483
|
+
* The write gate, plus the one extra fact a removal needs: this
|
|
484
|
+
* session's recorder has a `removeProp` at all (`writeProp`'s absence
|
|
485
|
+
* already refuses the write half).
|
|
486
|
+
*
|
|
487
|
+
* WHAT THIS ANSWER DOES NOT CLAIM, and cannot: whether the callsite
|
|
488
|
+
* CURRENTLY carries the attribute. This client holds an oid → file:line
|
|
489
|
+
* index, never the game's source text — the same structural fact that
|
|
490
|
+
* makes `anchor` honestly `null` for an oid the index has not resolved,
|
|
491
|
+
* and the reason `writeProp` sends the OID for the SERVER to resolve.
|
|
492
|
+
* The first-party R3F lane can read its own file and so answers both
|
|
493
|
+
* halves (`r3f-source-authoring-adapter.ts`'s `transformEditability`);
|
|
494
|
+
* this one answers for the DOOR and lets `perform` — which returns the
|
|
495
|
+
* server's own `changed` — answer for the drop. What it must never do is
|
|
496
|
+
* the inverse: ack a removal that took no byte, and it does not.
|
|
497
|
+
*/
|
|
498
|
+
available({ sourceOid, property }) {
|
|
499
|
+
if (!backend?.removeProp) return false;
|
|
500
|
+
return verdict(sourceOid, property).ok;
|
|
501
|
+
},
|
|
502
|
+
async perform(request) {
|
|
503
|
+
const decided = verdict(request.sourceOid, request.property);
|
|
504
|
+
if (!decided.ok || !backend?.removeProp) {
|
|
505
|
+
reportIngestSourceRefusal(
|
|
506
|
+
request.label,
|
|
507
|
+
decided.ok ? 'this editor tier cannot remove project source' : decided.reason,
|
|
508
|
+
);
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
const prop = JSX_PROP_BY_CHANNEL[request.property]!;
|
|
512
|
+
try {
|
|
513
|
+
const result = await backend.removeProp(request.sourceOid!, prop);
|
|
514
|
+
if (result.dynamic) {
|
|
515
|
+
reportIngestSourceRefusal(
|
|
516
|
+
request.label,
|
|
517
|
+
`${prop} at ${displayOf(request.sourceOid) ?? 'this element'} is bound to a JSX expression — removing it would delete the game's own wiring, not an override`,
|
|
518
|
+
);
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
if (!result.changed) {
|
|
522
|
+
reportIngestSourceRefusal(
|
|
523
|
+
request.label,
|
|
524
|
+
result.error ??
|
|
525
|
+
`${displayOf(request.sourceOid) ?? 'this element'} authors no ${prop} attribute, so there was nothing to drop`,
|
|
526
|
+
);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
editorConsole.log(
|
|
530
|
+
`[ingest] ${request.label} — ${prop} dropped from the game's own source at ${
|
|
531
|
+
displayOf(request.sourceOid) ?? request.sourceOid
|
|
532
|
+
}`,
|
|
533
|
+
);
|
|
534
|
+
// A removal moves the lines below it exactly as a write does.
|
|
535
|
+
void refreshIndex();
|
|
536
|
+
return true;
|
|
537
|
+
} catch (error) {
|
|
538
|
+
reportIngestSourceRefusal(
|
|
539
|
+
request.label,
|
|
540
|
+
`the removal failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
541
|
+
);
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
destination() {
|
|
547
|
+
return backend?.writeProp && editorIsAuthoring()
|
|
548
|
+
? OID_SOURCE_DESTINATION
|
|
549
|
+
: LIVE_ONLY_DESTINATION;
|
|
550
|
+
},
|
|
551
|
+
dispose() {
|
|
552
|
+
backend = null;
|
|
553
|
+
index = new Map();
|
|
554
|
+
indexLoaded = false;
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|