A shared value belongs to the run, so a declaration carries its scope (#91)
A variable or a state machine declared `global` keeps one value for the whole run instead of one per behavior instance. The scope sits on the declaration -- `PropertyDef.scope` and `StateMachineAsset.scope`, both optional, absence meaning `local` -- and `local` is never written, so every project made before this saves back unchanged. **The name is the key.** Two behaviors declaring a global `score` mean one variable; a behavior declaring `score` locally keeps its own. Kinds that disagree about one global name stop the run before it starts, with the two behaviors named; starting values that disagree are a warning on the behavior that declares it, and the first declaration wins (`globalConflicts`). The runtime needs no redirection layer. A global variable falls out of `schemaForObject`, so it never reaches `entity.props` -- and then the whole rule is "whoever declares the name takes their own value" (`graph/sharedState.ts`). The `Variables` node is untouched and stays transparent: switching a variable to global breaks no existing graph, because `GraphInterpreter.readAll` builds a fresh object per read anyway and only asks `port.key in all`. That same cut answers the inspector. A shared value has no owner, so the placed object shows no row for it and can hold no override -- `SetVariableScopeCommand` takes the deviations that were there and undo gives them back. Both places say it in words rather than leaving a glyph to be guessed: the declaration explains the mark below the list, and the placed object names the variable it no longer holds instead of claiming the behavior declares none -- which is what it said before, untruthfully. Screenshots of the e2e run turned that up; no check had been looking at it. `SharedState` hangs off `NodeContext`, is built in `BehaviorSystem.start()` from the *declarations* rather than from any instance (a global variable exists before its behavior is placed), and is thrown away with the run: nothing is written back, not even in the editor run. `GraphInterpreter.call` passes it on through `...ctx` while nested calls keep getting fresh local variables, which is why `program.defaults` is stripped of global keys. Two traps this hit. `defSignature`/`assetSignature` have to carry the scope or the inspector does not rebuild after the switch -- and a state machine has no rows at all, so its scope is appended to the signature by hand. And the object inspector does not go through `schemaForObject` for behaviors, it reads `asset.variables` directly; filtering in one place was not enough, and no unit test covered that path. The e2e caught it (`global: true` where false belonged), and the rule now lives in one named function, `objectRows`. Scopes `object` and `scene` are deliberately not built. The object case already has a route (`Object by name` -> `Properties`), and a run knows exactly one scene, so `scene` would be indistinguishable from `global` until #39 exists. Closes #59: a machine's scope is a word on the machine asset, not a setting per `Set state` node, so the same machine cannot live in two places at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9027a86edd
commit
d1fcfe8b81
27 files changed
+857
-45
No files matched your search
@@ -14,7 +14,7 @@ import {
|
||||
} from '../graph';
|
||||
import { isParticleNodeDef } from '../particles/nodes';
|
||||
import { isShaderNodeDef } from '../shader/nodes';
|
||||
import type { Asset, TextureFrame } from '../model/Asset';
|
||||
import type { Asset, StateMachineAsset, TextureFrame } from '../model/Asset';
|
||||
import { isAssetType } from '../model/AssetTypes';
|
||||
import { isFontFile } from '../model/font';
|
||||
import { emptyEdit, type InstanceEdit, isEmptyEdit } from '../model/instance';
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
tagOf,
|
||||
} from '../model/Properties';
|
||||
import { PROJECT_FORMAT_VERSION, type Project } from '../model/Project';
|
||||
import { readScope } from '../model/Scope';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { isTextureFile, normalizeFrameRect } from '../model/texture';
|
||||
import { hasParentCycle } from '../model/walk';
|
||||
@@ -130,7 +131,15 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
graph: normalizeGraph(a.graph),
|
||||
});
|
||||
} else if (type === 'statemachine') {
|
||||
assets.push({ id, type, name, props, graph: normalizeGraph(a.graph, (def) => def.type === STATE) });
|
||||
const machine: StateMachineAsset = {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
props,
|
||||
graph: normalizeGraph(a.graph, (def) => def.type === STATE),
|
||||
};
|
||||
if (readScope(a.scope) === 'global') machine.scope = 'global';
|
||||
assets.push(machine);
|
||||
} else if (type === 'shader') {
|
||||
assets.push({ id, type, name, props, graph: shaderGraphs.get(id) ?? createEmptyGraph() });
|
||||
} else if (type === 'particles') {
|
||||
@@ -251,6 +260,8 @@ function normalizeVariables(raw: unknown): PropertyDef[] {
|
||||
const fields = normalizeStructFields(v.fields);
|
||||
const def = variableDef(key, kind, undefined, fields);
|
||||
def.default = coerceValue(def, (v.default ?? def.default) as PropertyValue);
|
||||
// Written only when shared, so every project made before scopes existed saves back unchanged.
|
||||
if (readScope(v.scope) === 'global') def.scope = 'global';
|
||||
defs.push(def);
|
||||
}
|
||||
return defs;
|
||||
|
||||
Reference in new issue
Block a user