@neocompose/cli 0.74.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.75.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0a7ec0e: `member.OnChanged.Clear()` removes every handler from a member's or getter's listener set. On a stored member it clears `@listeners` defaults and Save and Session wiring. On a getter it ends the subscription. Any other use of `OnChanged` as a value now reports "OnChanged supports only '+=', '-=', and Clear()." instead of "Cannot read member 'OnChanged'". Exports move to schema 41, so Unity needs SDK 0.65.0.
8
+
3
9
  ## 0.74.0
4
10
 
5
11
  ### Minor Changes
package/dist/neo.mjs CHANGED
@@ -3150,24 +3150,30 @@ function isNSInstructionTry(value) {
3150
3150
  }
3151
3151
  return true;
3152
3152
  }
3153
- function isNSInstructionChangeListener(value) {
3154
- const instruction = value;
3155
- if (instruction?.type !== "addChangeListener" /* addChangeListener */ && instruction?.type !== "removeChangeListener" /* removeChangeListener */)
3156
- return false;
3157
- const target = instruction.target;
3153
+ function isNSChangeListenerTarget(value) {
3154
+ const target = value;
3158
3155
  if (!target || typeof target.memberId !== "string" || target.memberId.length === 0)
3159
3156
  return false;
3160
3157
  if (!isNSPointer(target.owner) || !isNSTypeInfo(target.typeInfo))
3161
3158
  return false;
3162
- if (!Object.values(NSWritability).includes(target.writability)) return false;
3163
- return isNSPointer(instruction.listener);
3159
+ return target.writability !== void 0 && Object.values(NSWritability).includes(target.writability);
3160
+ }
3161
+ function isNSInstructionChangeListener(value) {
3162
+ const instruction = value;
3163
+ if (instruction?.type !== "addChangeListener" /* addChangeListener */ && instruction?.type !== "removeChangeListener" /* removeChangeListener */)
3164
+ return false;
3165
+ return isNSChangeListenerTarget(instruction.target) && isNSPointer(instruction.listener);
3166
+ }
3167
+ function isNSInstructionClearChangeListeners(value) {
3168
+ const instruction = value;
3169
+ return instruction?.type === "clearChangeListeners" /* clearChangeListeners */ && isNSChangeListenerTarget(instruction.target);
3164
3170
  }
3165
3171
  function isNSInstructionDebug(value) {
3166
3172
  const v = value;
3167
3173
  return v?.type === "debug" /* debug */ && ["log", "warning", "error", "assert"].includes(v.severity ?? "") && isNSPointer(v.message) && isNSTypeInfo(v.messageType) && (v.severity === "assert" ? isNSPointer(v.condition) : v.condition === void 0);
3168
3174
  }
3169
3175
  function isNSInstruction(value) {
3170
- return isNSInstructionDebug(value) || isNSInstructionVariable(value) || isNSInstructionIfBranch(value) || isNSInstructionReturn(value) || isNSInstructionThrow(value) || isNSInstructionAssign(value) || isNSInstructionCollectionCall(value) || isNSInstructionFunctionCall(value) || isNSInstructionConditionLoop(value) || isNSInstructionFor(value) || isNSInstructionForEach(value) || isNSInstructionBreak(value) || isNSInstructionContinue(value) || isNSInstructionSwitch(value) || isNSInstructionTry(value) || isNSInstructionChangeListener(value) || isNSInstructionAddActionListener(value) || isNSInstructionRemoveActionListener(value);
3176
+ return isNSInstructionDebug(value) || isNSInstructionVariable(value) || isNSInstructionIfBranch(value) || isNSInstructionReturn(value) || isNSInstructionThrow(value) || isNSInstructionAssign(value) || isNSInstructionCollectionCall(value) || isNSInstructionFunctionCall(value) || isNSInstructionConditionLoop(value) || isNSInstructionFor(value) || isNSInstructionForEach(value) || isNSInstructionBreak(value) || isNSInstructionContinue(value) || isNSInstructionSwitch(value) || isNSInstructionTry(value) || isNSInstructionChangeListener(value) || isNSInstructionClearChangeListeners(value) || isNSInstructionAddActionListener(value) || isNSInstructionRemoveActionListener(value);
3171
3177
  }
3172
3178
  function isNSInstructions(value) {
3173
3179
  return Array.isArray(value) && value.every(isNSInstruction);
@@ -30812,7 +30818,7 @@ function intersectCollectionWritability(left, right) {
30812
30818
  const writable = (value) => value === "local" /* Local */ || value === "save" /* Save */ || value === "session" /* Session */ || value === "runtime" /* Runtime */;
30813
30819
  return writable(left) && writable(right) ? "runtime" /* Runtime */ : "readOnly" /* ReadOnly */;
30814
30820
  }
30815
- var CONSTRUCTING_FUNCTION_KINDS, VARIANT_INITIALIZE_NAME, VARIANT_APPLY_NAME, MATH_NAMESPACE_NAME, MATH_FUNCTIONS, MATH_CONSTANTS, LIST_STATIC_FUNCTIONS, WRITE_TARGET_DESCRIPTIONS, FALLTHROUGH_LABELS, Scope, UnresolvedPersistedListenerReference, StrictNeoScriptResolver, SPRITE_DERIVED_MEMBERS;
30821
+ var CONSTRUCTING_FUNCTION_KINDS, VARIANT_INITIALIZE_NAME, VARIANT_APPLY_NAME, MATH_NAMESPACE_NAME, MATH_FUNCTIONS, MATH_CONSTANTS, LIST_STATIC_FUNCTIONS, WRITE_TARGET_DESCRIPTIONS, ON_CHANGED_OPERATIONS_ERROR, FALLTHROUGH_LABELS, Scope, UnresolvedPersistedListenerReference, StrictNeoScriptResolver, SPRITE_DERIVED_MEMBERS;
30816
30822
  var init_strict_resolver = __esm({
30817
30823
  "../packages/neoscript-language/src/strict-resolver.ts"() {
30818
30824
  "use strict";
@@ -30873,6 +30879,7 @@ var init_strict_resolver = __esm({
30873
30879
  projectValue: "a stored project value",
30874
30880
  unknown: "something other than `this` or a local"
30875
30881
  };
30882
+ ON_CHANGED_OPERATIONS_ERROR = "OnChanged supports only '+=', '-=', and Clear().";
30876
30883
  FALLTHROUGH_LABELS = {
30877
30884
  function: "NSFunction",
30878
30885
  getter: "getter",
@@ -32215,6 +32222,12 @@ var init_strict_resolver = __esm({
32215
32222
  this.resolveExpression(statement.expr, scope);
32216
32223
  const debug = this.resolveDebugStatement(statement.expr, scope);
32217
32224
  if (debug) return debug;
32225
+ const listenerClear = this.resolveChangeListenerClear(
32226
+ statement.expr,
32227
+ scope,
32228
+ statement.pos
32229
+ );
32230
+ if (listenerClear) return listenerClear;
32218
32231
  const actionClear = this.resolveActionClear(
32219
32232
  statement.expr,
32220
32233
  scope,
@@ -32575,8 +32588,20 @@ var init_strict_resolver = __esm({
32575
32588
  const observed = this.resolveExpression(statement.target.receiver, scope);
32576
32589
  const observedType = neoScriptMemberLookupType(observed.type);
32577
32590
  const declared = observedType.kind === "named" && this.project.typeById.get(observedType.typeId)?.members.some((member) => member.name === "OnChanged");
32578
- if (!declared)
32579
- return this.resolveChangeSubscription(statement, observed, scope);
32591
+ if (!declared) {
32592
+ if (statement.op !== "+=" && statement.op !== "-=")
32593
+ throw new CompileError(ON_CHANGED_OPERATIONS_ERROR, statement.pos);
32594
+ return this.resolveChangeListener(
32595
+ statement.target,
32596
+ observed,
32597
+ scope,
32598
+ statement.pos,
32599
+ {
32600
+ kind: statement.op === "+=" ? "addChangeListener" /* AddChangeListener */ : "removeChangeListener" /* RemoveChangeListener */,
32601
+ handler: statement.value
32602
+ }
32603
+ );
32604
+ }
32580
32605
  }
32581
32606
  const local = statement.target.kind === "ident" ? scope.lookup(statement.target.name) : null;
32582
32607
  if (local?.readonlyBinding) {
@@ -32760,158 +32785,142 @@ var init_strict_resolver = __esm({
32760
32785
  pos
32761
32786
  );
32762
32787
  }
32763
- resolveChangeSubscription(statement, observed, scope) {
32764
- if (statement.op !== "+=" && statement.op !== "-=") {
32765
- throw new CompileError(
32766
- "OnChanged supports only '+=' and '-=' subscriptions.",
32767
- statement.pos
32768
- );
32769
- }
32770
- if (statement.target.kind !== "member") {
32788
+ /** `X.M.OnChanged.Clear()` empties M's listener set on X (P70, P106). */
32789
+ resolveChangeListenerClear(expression, scope, pos) {
32790
+ if (expression.kind !== "call" || expression.callee.kind !== "member" || expression.callee.name !== "Clear" || expression.callee.receiver.kind !== "member" || expression.callee.receiver.name !== "OnChanged")
32791
+ return null;
32792
+ const onChanged = expression.callee.receiver;
32793
+ const observed = this.resolveExpression(onChanged.receiver, scope);
32794
+ const observedType = neoScriptMemberLookupType(observed.type);
32795
+ if (observedType.kind === "named" && this.project.typeById.get(observedType.typeId)?.members.some((member) => member.name === "OnChanged"))
32796
+ return null;
32797
+ if (expression.callee.optional)
32771
32798
  throw new CompileError(
32772
- "OnChanged requires a declared member slot.",
32773
- statement.pos
32799
+ "OnChanged is never null, so optional-chained Clear is not supported.",
32800
+ pos
32774
32801
  );
32775
- }
32776
- if (statement.target.optional) {
32802
+ requireArgCount("Clear", expression.args, 0, pos);
32803
+ return this.resolveChangeListener(onChanged, observed, scope, pos, {
32804
+ kind: "clearChangeListeners" /* ClearChangeListeners */
32805
+ });
32806
+ }
32807
+ resolveChangeListener(onChanged, observed, scope, pos, edit) {
32808
+ if (onChanged.optional) {
32777
32809
  throw new CompileError(
32778
32810
  "OnChanged cannot use an optional subscription target.",
32779
- statement.pos
32811
+ pos
32780
32812
  );
32781
32813
  }
32782
32814
  if (observed.optionalChainType !== void 0) {
32783
32815
  throw new CompileError(
32784
32816
  "OnChanged cannot subscribe through optional chaining; narrow the receiver first.",
32785
- statement.pos
32817
+ pos
32786
32818
  );
32787
32819
  }
32788
32820
  const member = observed.symbol;
32789
32821
  if (!member)
32790
32822
  throw new CompileError(
32791
32823
  "OnChanged requires a declared member, not a local variable.",
32792
- statement.pos
32824
+ pos
32793
32825
  );
32794
32826
  if (observed.pointer.type === "staticMember" /* StaticMember */ || observed.pointer.type === "callGetter" /* CallGetter */ && observed.pointer.receiver.kind === "static") {
32795
- const access = statement.target.receiver;
32827
+ const access = onChanged.receiver;
32796
32828
  const name = access.kind === "member" && access.receiver.kind === "ident" ? `${access.receiver.name}.${member.name}` : member.name;
32797
32829
  throw new CompileError(
32798
32830
  `OnChanged requires an instance member. '${name}' is static.`,
32799
- statement.pos
32831
+ pos
32800
32832
  );
32801
32833
  }
32802
32834
  if (observed.pointer.type === "callGetter" /* CallGetter */)
32803
- return this.resolveGetterSubscription(
32804
- statement,
32805
- statement.target.receiver,
32835
+ return this.resolveGetterChangeListener(
32836
+ onChanged.receiver,
32806
32837
  observed,
32807
32838
  observed.pointer,
32808
32839
  member,
32809
- scope
32840
+ scope,
32841
+ pos,
32842
+ edit
32810
32843
  );
32811
32844
  if (observed.pointer.type !== "keyOf" /* KeyOf */)
32812
- throw new CompileError(
32813
- "OnChanged requires a declared member slot.",
32814
- statement.pos
32815
- );
32845
+ throw new CompileError("OnChanged requires a declared member slot.", pos);
32816
32846
  if (!observed.pointer.memberId)
32817
32847
  throw new CompileError(
32818
32848
  "OnChanged cannot observe a collection entry.",
32819
- statement.pos
32849
+ pos
32820
32850
  );
32821
32851
  if (member.computed) {
32822
32852
  throw new CompileError(
32823
32853
  "OnChanged cannot observe a computed member.",
32824
- statement.pos
32854
+ pos
32825
32855
  );
32826
32856
  }
32827
32857
  if (member.readonlyDeclaration) {
32828
32858
  throw new CompileError(
32829
32859
  "OnChanged cannot observe a readonly declaration.",
32830
- statement.pos
32860
+ pos
32831
32861
  );
32832
32862
  }
32833
32863
  if (observed.type.kind === "delegate" || observed.type.kind === "action") {
32834
32864
  throw new CompileError(
32835
32865
  "OnChanged cannot observe a callable member.",
32836
- statement.pos
32866
+ pos
32837
32867
  );
32838
32868
  }
32839
32869
  const writability = observed.userData ? "runtime" /* Runtime */ : observed.writability;
32840
32870
  if (writability !== "save" /* Save */ && writability !== "session" /* Session */ && writability !== "runtime" /* Runtime */ && writability !== "local" /* Local */) {
32841
32871
  throw new CompileError(
32842
32872
  "OnChanged requires a member whose storage is writable at gameplay runtime.",
32843
- statement.pos
32873
+ pos
32844
32874
  );
32845
32875
  }
32846
32876
  this.validateConstructorWriteTarget(
32847
32877
  observed.pointer,
32848
- "subscribe to",
32849
- statement.pos,
32878
+ edit.kind === "clearChangeListeners" /* ClearChangeListeners */ ? "clear listeners on" : "subscribe to",
32879
+ pos,
32850
32880
  scope,
32851
32881
  false
32852
32882
  );
32853
- if (!statement.value) {
32854
- throw new CompileError(
32855
- "OnChanged subscription requires a handler member.",
32856
- statement.pos
32857
- );
32858
- }
32859
32883
  const storageType = this.assignmentStorageType(
32860
- statement.target.receiver,
32884
+ onChanged.receiver,
32861
32885
  observed,
32862
32886
  null,
32863
32887
  scope
32864
32888
  );
32865
- const expected = {
32866
- kind: "action",
32867
- parameterTypes: [storageType]
32868
- };
32869
- const listener = this.resolveActionListener(
32870
- statement.value,
32871
- expected,
32872
- scope,
32873
- true,
32874
- true
32875
- );
32876
- return {
32877
- type: statement.op === "+=" ? "addChangeListener" /* AddChangeListener */ : "removeChangeListener" /* RemoveChangeListener */,
32878
- target: {
32889
+ return this.changeListenerInstruction(
32890
+ edit,
32891
+ {
32879
32892
  owner: observed.pointer.keyOf.pointer,
32880
32893
  memberId: member.canonicalMemberId ?? observed.pointer.memberId,
32881
32894
  typeInfo: toWireType(storageType, this.project),
32882
32895
  writability
32883
32896
  },
32884
- listener
32885
- };
32897
+ storageType,
32898
+ scope,
32899
+ pos
32900
+ );
32886
32901
  }
32887
32902
  /**
32888
32903
  * P106. A getter subscription lives only in the running client's memory:
32889
32904
  * nothing persists it, so construction replay would silently drop one.
32890
32905
  */
32891
- resolveGetterSubscription(statement, getterRead, observed, pointer, member, scope) {
32906
+ resolveGetterChangeListener(getterRead, observed, pointer, member, scope, pos, edit) {
32892
32907
  if (pointer.dispatch === "base") {
32893
32908
  throw new CompileError(
32894
32909
  `OnChanged can't observe a base getter. Subscribe to '${member.name}' on the instance.`,
32895
- statement.pos
32910
+ pos
32896
32911
  );
32897
32912
  }
32898
32913
  if (pointer.receiver.kind !== "instance") {
32899
32914
  throw new CompileError(
32900
32915
  `OnChanged requires an instance receiver for getter '${member.name}'.`,
32901
- statement.pos
32916
+ pos
32902
32917
  );
32903
32918
  }
32904
32919
  const kind = this.context.kind;
32905
32920
  if (isNeoScriptConstructionDocumentKind(kind) || kind === "migration") {
32906
32921
  throw new CompileError(
32907
- `A getter subscription lives only for the play session, so ${dialogueBodyKindLabel(kind)} can't make one. Subscribe from a function.`,
32908
- statement.pos
32909
- );
32910
- }
32911
- if (!statement.value) {
32912
- throw new CompileError(
32913
- "OnChanged subscription requires a handler member.",
32914
- statement.pos
32922
+ edit.kind === "clearChangeListeners" /* ClearChangeListeners */ ? `A getter subscription lives only for the play session, so ${dialogueBodyKindLabel(kind)} can't clear one. Clear from a function.` : `A getter subscription lives only for the play session, so ${dialogueBodyKindLabel(kind)} can't make one. Subscribe from a function.`,
32923
+ pos
32915
32924
  );
32916
32925
  }
32917
32926
  const getterType = this.assignmentStorageType(
@@ -32920,23 +32929,36 @@ var init_strict_resolver = __esm({
32920
32929
  null,
32921
32930
  scope
32922
32931
  );
32923
- const listener = this.resolveActionListener(
32924
- statement.value,
32925
- { kind: "action", parameterTypes: [getterType] },
32926
- scope,
32927
- true,
32928
- true
32929
- );
32930
- return {
32931
- type: statement.op === "+=" ? "addChangeListener" /* AddChangeListener */ : "removeChangeListener" /* RemoveChangeListener */,
32932
- target: {
32932
+ return this.changeListenerInstruction(
32933
+ edit,
32934
+ {
32933
32935
  owner: pointer.receiver.pointer,
32934
32936
  memberId: member.canonicalMemberId ?? pointer.memberId,
32935
32937
  typeInfo: toWireType(getterType, this.project),
32936
32938
  writability: "session" /* Session */
32937
32939
  },
32938
- listener
32939
- };
32940
+ getterType,
32941
+ scope,
32942
+ pos
32943
+ );
32944
+ }
32945
+ changeListenerInstruction(edit, target, observedType, scope, pos) {
32946
+ if (edit.kind === "clearChangeListeners" /* ClearChangeListeners */)
32947
+ return { type: edit.kind, target };
32948
+ if (!edit.handler) {
32949
+ throw new CompileError(
32950
+ "OnChanged subscription requires a handler member.",
32951
+ pos
32952
+ );
32953
+ }
32954
+ const listener = this.resolveActionListener(
32955
+ edit.handler,
32956
+ { kind: "action", parameterTypes: [observedType] },
32957
+ scope,
32958
+ true,
32959
+ true
32960
+ );
32961
+ return { type: edit.kind, target, listener };
32940
32962
  }
32941
32963
  /**
32942
32964
  * P62 §3.2. `+=` / `-=` on an NSAction member are subscription operators,
@@ -35054,6 +35076,8 @@ var init_strict_resolver = __esm({
35054
35076
  entryUserData
35055
35077
  };
35056
35078
  }
35079
+ if (name === "OnChanged")
35080
+ throw new CompileError(ON_CHANGED_OPERATIONS_ERROR, pos);
35057
35081
  throw new CompileError(
35058
35082
  `Cannot read member '${name}' on ${this.describe(receiver.type)}`,
35059
35083
  pos
@@ -51773,7 +51797,7 @@ var init_registry = __esm({
51773
51797
  "../packages/manifest/schema-contract/registry.mjs"() {
51774
51798
  "use strict";
51775
51799
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
51776
- cliVersion: "0.74.0",
51800
+ cliVersion: "0.75.0",
51777
51801
  projectFileUploadBatchSize: 32,
51778
51802
  documentRecords: {
51779
51803
  member: {
@@ -78978,6 +79002,9 @@ function listenerWritesByOwner(state) {
78978
79002
  }
78979
79003
  return index.owners;
78980
79004
  }
79005
+ function changeListenerWriteEdit(write) {
79006
+ return write.operation === "clear" ? { kind: "replace", targets: [] } : { kind: write.operation, target: write.listener };
79007
+ }
78981
79008
  var indexes, empty;
78982
79009
  var init_listener_write_index = __esm({
78983
79010
  "../src/runtime/listener-write-index.ts"() {
@@ -81888,33 +81915,41 @@ function finalizeConstructorAllocations(ctx, returnValue, ownsInvocationState, r
81888
81915
  const listenerEdges = /* @__PURE__ */ new Map();
81889
81916
  for (const write of state.writes) {
81890
81917
  if (!retainListenerReceivers || write.kind !== "changeListener") continue;
81918
+ const slotKey = JSON.stringify([write.ownerId, write.memberId]);
81919
+ if (write.operation === "clear") {
81920
+ listenerEdges.delete(slotKey);
81921
+ continue;
81922
+ }
81891
81923
  const key = JSON.stringify([
81892
- write.ownerId,
81893
- write.memberId,
81894
81924
  write.listener.memberId,
81895
81925
  write.listener.valueId
81896
81926
  ]);
81897
- if (write.operation === "remove") listenerEdges.delete(key);
81898
- else listenerEdges.set(key, write);
81927
+ let edges = listenerEdges.get(slotKey);
81928
+ if (write.operation === "remove") edges?.delete(key);
81929
+ else {
81930
+ if (edges === void 0) listenerEdges.set(slotKey, edges = /* @__PURE__ */ new Map());
81931
+ edges.set(key, write);
81932
+ }
81899
81933
  }
81900
81934
  const externalListenerReceivers = /* @__PURE__ */ new Set();
81901
- for (const write of listenerEdges.values()) {
81902
- if (write.listener.valueId === null) continue;
81903
- const receiverGroup = groupByRowId.get(write.listener.valueId);
81904
- if (receiverGroup === void 0) continue;
81905
- const ownerGroup = groupByRowId.get(write.ownerId);
81906
- if (ownerGroup === void 0) {
81907
- if (rowForCleanup(write.ownerId) !== null)
81908
- externalListenerReceivers.add(receiverGroup);
81909
- } else {
81910
- let dependencies = state.constructorGroupDependencies.get(ownerGroup);
81911
- if (dependencies === void 0) {
81912
- dependencies = /* @__PURE__ */ new Set();
81913
- state.constructorGroupDependencies.set(ownerGroup, dependencies);
81935
+ for (const edges of listenerEdges.values())
81936
+ for (const write of edges.values()) {
81937
+ if (write.listener.valueId === null) continue;
81938
+ const receiverGroup = groupByRowId.get(write.listener.valueId);
81939
+ if (receiverGroup === void 0) continue;
81940
+ const ownerGroup = groupByRowId.get(write.ownerId);
81941
+ if (ownerGroup === void 0) {
81942
+ if (rowForCleanup(write.ownerId) !== null)
81943
+ externalListenerReceivers.add(receiverGroup);
81944
+ } else {
81945
+ let dependencies = state.constructorGroupDependencies.get(ownerGroup);
81946
+ if (dependencies === void 0) {
81947
+ dependencies = /* @__PURE__ */ new Set();
81948
+ state.constructorGroupDependencies.set(ownerGroup, dependencies);
81949
+ }
81950
+ dependencies.add(receiverGroup);
81914
81951
  }
81915
- dependencies.add(receiverGroup);
81916
81952
  }
81917
- }
81918
81953
  const markConstructorGroupEscaped = (rootId) => {
81919
81954
  if (escapedRoots.has(rootId)) return;
81920
81955
  escapedRoots.add(rootId);
@@ -82111,7 +82146,7 @@ function finalizeConstructorAllocations(ctx, returnValue, ownsInvocationState, r
82111
82146
  const survivingWrites = state.writes.filter((write) => {
82112
82147
  if (write.kind === "changeListener") {
82113
82148
  if (collectedIds.has(write.ownerId)) return false;
82114
- return write.listener.valueId === null || !collectedIds.has(write.listener.valueId);
82149
+ return write.operation === "clear" || write.listener.valueId === null || !collectedIds.has(write.listener.valueId);
82115
82150
  }
82116
82151
  const destination = write.resolvedDestination;
82117
82152
  const destinationId = destination !== void 0 && "containerValueId" in destination ? destination.containerValueId : void 0;
@@ -82752,7 +82787,8 @@ function evalInstructions(instructions, scope, ctx, options) {
82752
82787
  break;
82753
82788
  }
82754
82789
  case "addChangeListener" /* addChangeListener */:
82755
- case "removeChangeListener" /* removeChangeListener */: {
82790
+ case "removeChangeListener" /* removeChangeListener */:
82791
+ case "clearChangeListeners" /* clearChangeListeners */: {
82756
82792
  evalChangeListenerInstruction(ins, scope, ctx, options);
82757
82793
  break;
82758
82794
  }
@@ -83182,6 +83218,18 @@ function evalChangeListenerInstruction(ins, scope, ctx, options) {
83182
83218
  throw new NSGetterRuntimeError(
83183
83219
  `OnChanged member '${ins.target.memberId}' is not an observable member or getter of owner '${owner.id}'.`
83184
83220
  );
83221
+ if (ins.type === "clearChangeListeners" /* clearChangeListeners */) {
83222
+ if (getter !== null) clearGetterSubscription(getter, ctx);
83223
+ else
83224
+ options.writes.push({
83225
+ kind: "changeListener",
83226
+ target: ins.target,
83227
+ ownerId: owner.id,
83228
+ memberId: slot.memberId,
83229
+ operation: "clear"
83230
+ });
83231
+ return;
83232
+ }
83185
83233
  const listener = evalPointer(ins.listener, scope, ctx);
83186
83234
  if (!isMemberDelegateTarget(listener))
83187
83235
  throw new NSGetterRuntimeError(
@@ -83238,6 +83286,17 @@ function editGetterSubscription(slot, target, add, ctx) {
83238
83286
  }
83239
83287
  subscription.targets.push(target);
83240
83288
  }
83289
+ function clearGetterSubscription(slot, ctx) {
83290
+ if ((ctx.__executionState?.constructionStack.length ?? 0) > 0)
83291
+ throw new NSGetterRuntimeError(
83292
+ "A getter subscription lives only for the play session, so a constructor can't clear one. Clear from a function."
83293
+ );
83294
+ const subscriptions = ctx.__getterSubscriptions;
83295
+ const subscription = subscriptions?.get(slot.owner.id, slot.memberId);
83296
+ if (subscriptions === void 0 || subscription === void 0) return;
83297
+ subscription.targets.length = 0;
83298
+ subscriptions.drop(subscription);
83299
+ }
83241
83300
  function dispatchGetterSubscriptions(ctx) {
83242
83301
  const subscriptions = ctx.__getterSubscriptions;
83243
83302
  if (subscriptions === void 0) return;
@@ -91540,7 +91599,7 @@ function cloneClassValueGraph(receiver, expectedClassId, ctx) {
91540
91599
  const edit = editChangeListenerSet(
91541
91600
  entry?.[write.memberId],
91542
91601
  [],
91543
- { kind: write.operation, target: write.listener },
91602
+ changeListenerWriteEdit(write),
91544
91603
  { inheritedComplete: false }
91545
91604
  );
91546
91605
  if (!edit.changed) continue;
@@ -109013,10 +109072,11 @@ function migrationMemberListenerRows(document, writes, copied) {
109013
109072
  const map = root.changeListeners;
109014
109073
  const entry = map[write.ownerId] ?? {};
109015
109074
  const inherited = reader.listenerDefaultsForOwner(write.ownerId)?.[write.memberId] ?? [];
109016
- const edit = editChangeListenerSet(entry[write.memberId], inherited, {
109017
- kind: write.operation,
109018
- target: write.listener
109019
- });
109075
+ const edit = editChangeListenerSet(
109076
+ entry[write.memberId],
109077
+ inherited,
109078
+ changeListenerWriteEdit(write)
109079
+ );
109020
109080
  if (!edit.changed) continue;
109021
109081
  if (edit.targets === void 0) delete entry[write.memberId];
109022
109082
  else entry[write.memberId] = edit.targets;
@@ -109049,6 +109109,7 @@ var init_project_migration_member_listeners = __esm({
109049
109109
  init_members();
109050
109110
  init_project_root_members();
109051
109111
  init_member_change_listeners();
109112
+ init_listener_write_index();
109052
109113
  init_authored_listener_endpoints();
109053
109114
  }
109054
109115
  });
@@ -172172,7 +172233,7 @@ var init_project_unity_export_types = __esm({
172172
172233
  "../src/models/exports/project-unity-export-types.ts"() {
172173
172234
  "use strict";
172174
172235
  init_unity_codegen_revision();
172175
- PROJECT_UNITY_EXPORT_SCHEMA_VERSION = 40;
172236
+ PROJECT_UNITY_EXPORT_SCHEMA_VERSION = 41;
172176
172237
  }
172177
172238
  });
172178
172239
 
@@ -189159,7 +189220,7 @@ var init_member_change_listener_runtime = __esm({
189159
189220
  for (const ownerId of owners)
189160
189221
  for (const write of writes.get(ownerId) ?? []) {
189161
189222
  const slot = this.slot(write.ownerId, write.memberId);
189162
- if (slot === null || this.targetTier(slot, write.listener)?.tier !== "save")
189223
+ if (slot === null || write.operation !== "clear" && this.targetTier(slot, write.listener)?.tier !== "save")
189163
189224
  continue;
189164
189225
  const entry = map[write.ownerId] ?? {};
189165
189226
  const inherited = this.baseline(slot, "save");
@@ -189173,7 +189234,7 @@ var init_member_change_listener_runtime = __esm({
189173
189234
  const edited = editChangeListenerSet(
189174
189235
  durable,
189175
189236
  inherited.targets,
189176
- { kind: write.operation, target: write.listener },
189237
+ changeListenerWriteEdit(write),
189177
189238
  { inheritedComplete: inherited.complete }
189178
189239
  );
189179
189240
  if (!edited.changed) continue;
@@ -189265,41 +189326,48 @@ var init_member_change_listener_runtime = __esm({
189265
189326
  throw new Error(
189266
189327
  `OnChanged slot '${write.ownerId}/${write.memberId}' is not writable in this runtime.`
189267
189328
  );
189268
- let target;
189329
+ const targets = [];
189269
189330
  if (write.operation === "reset")
189270
- target = { tier: write.tier, edit: { kind: "reset" } };
189271
- else {
189331
+ targets.push({ tier: write.tier, edit: { kind: "reset" } });
189332
+ else if (write.operation === "clear") {
189333
+ const edit = changeListenerWriteEdit(write);
189334
+ if (slot.storage !== 3 /* Session */)
189335
+ targets.push({ tier: "save", edit });
189336
+ targets.push({ tier: "session", edit });
189337
+ } else {
189272
189338
  const resolved = this.targetTier(slot, write.listener);
189273
189339
  if (resolved === null)
189274
189340
  throw new Error(
189275
189341
  `OnChanged handler '${write.listener.memberId}' is incompatible with '${write.ownerId}/${write.memberId}'.`
189276
189342
  );
189277
189343
  const inSession = current("session", slot.rootId, slot.owner.id)?.[slot.memberId]?.some(
189278
- (existing2) => nsActionListenerIdentity(existing2) === nsActionListenerIdentity(resolved.target)
189344
+ (existing) => nsActionListenerIdentity(existing) === nsActionListenerIdentity(resolved.target)
189279
189345
  );
189280
- target = {
189346
+ targets.push({
189281
189347
  tier: inSession ? "session" : resolved.tier,
189282
189348
  edit: { kind: write.operation, target: resolved.target }
189283
- };
189349
+ });
189350
+ }
189351
+ for (const target of targets) {
189352
+ this.host.assertWritable(slot, target.tier);
189353
+ const existing = current(target.tier, slot.rootId, slot.owner.id);
189354
+ const explicit = existing !== void 0 && Object.hasOwn(existing, slot.memberId) ? existing[slot.memberId] : void 0;
189355
+ const baseline = this.baseline(
189356
+ slot,
189357
+ target.tier,
189358
+ target.tier !== "save" || !adopting.has(slot.owner.id)
189359
+ );
189360
+ const result = editChangeListenerSet(
189361
+ explicit,
189362
+ baseline.targets,
189363
+ target.edit,
189364
+ { inheritedComplete: baseline.complete }
189365
+ );
189366
+ if (!result.changed) continue;
189367
+ const entry = editOwner(target.tier, slot.rootId, slot.owner.id);
189368
+ if (result.targets === void 0) delete entry[slot.memberId];
189369
+ else setOwnField(entry, slot.memberId, result.targets);
189284
189370
  }
189285
- this.host.assertWritable(slot, target.tier);
189286
- const existing = current(target.tier, slot.rootId, slot.owner.id);
189287
- const explicit = existing !== void 0 && Object.hasOwn(existing, slot.memberId) ? existing[slot.memberId] : void 0;
189288
- const baseline = this.baseline(
189289
- slot,
189290
- target.tier,
189291
- target.tier !== "save" || !adopting.has(slot.owner.id)
189292
- );
189293
- const result = editChangeListenerSet(
189294
- explicit,
189295
- baseline.targets,
189296
- target.edit,
189297
- { inheritedComplete: baseline.complete }
189298
- );
189299
- if (!result.changed) continue;
189300
- const entry = editOwner(target.tier, slot.rootId, slot.owner.id);
189301
- if (result.targets === void 0) delete entry[slot.memberId];
189302
- else setOwnField(entry, slot.memberId, result.targets);
189303
189371
  }
189304
189372
  for (const tier of ["save", "session"]) {
189305
189373
  for (const [rootId, changed] of pending[tier]) {
@@ -190173,7 +190241,7 @@ var init_dialogue_dryrun_listeners = __esm({
190173
190241
  this.copied = context.__copiedChangeListeners ?? this.copied;
190174
190242
  this.runtime.retireOwners(context.__removedValueIds ?? []);
190175
190243
  const listenerWrites = evaluatedWrites(result).filter(
190176
- (write) => write.kind === "changeListener" && !context.__removedValueIds?.has(write.ownerId) && (write.listener.valueId === null || !context.__removedValueIds?.has(write.listener.valueId))
190244
+ (write) => write.kind === "changeListener" && !context.__removedValueIds?.has(write.ownerId) && (write.operation === "clear" || write.listener.valueId === null || !context.__removedValueIds?.has(write.listener.valueId))
190177
190245
  );
190178
190246
  const adopted = new Set(
190179
190247
  (result.createdSessionValues ?? []).map((row) => row.id)
@@ -191907,7 +191975,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
191907
191975
  async function main() {
191908
191976
  const args = parseArgs(process.argv.slice(2));
191909
191977
  if (args.command === "--version") {
191910
- console.log("0.74.0");
191978
+ console.log("0.75.0");
191911
191979
  return;
191912
191980
  }
191913
191981
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {
@@ -3629,24 +3629,30 @@ function isNSInstructionTry(value) {
3629
3629
  }
3630
3630
  return true;
3631
3631
  }
3632
- function isNSInstructionChangeListener(value) {
3633
- const instruction = value;
3634
- if (instruction?.type !== "addChangeListener" /* addChangeListener */ && instruction?.type !== "removeChangeListener" /* removeChangeListener */)
3635
- return false;
3636
- const target = instruction.target;
3632
+ function isNSChangeListenerTarget(value) {
3633
+ const target = value;
3637
3634
  if (!target || typeof target.memberId !== "string" || target.memberId.length === 0)
3638
3635
  return false;
3639
3636
  if (!isNSPointer(target.owner) || !isNSTypeInfo(target.typeInfo))
3640
3637
  return false;
3641
- if (!Object.values(NSWritability).includes(target.writability)) return false;
3642
- return isNSPointer(instruction.listener);
3638
+ return target.writability !== void 0 && Object.values(NSWritability).includes(target.writability);
3639
+ }
3640
+ function isNSInstructionChangeListener(value) {
3641
+ const instruction = value;
3642
+ if (instruction?.type !== "addChangeListener" /* addChangeListener */ && instruction?.type !== "removeChangeListener" /* removeChangeListener */)
3643
+ return false;
3644
+ return isNSChangeListenerTarget(instruction.target) && isNSPointer(instruction.listener);
3645
+ }
3646
+ function isNSInstructionClearChangeListeners(value) {
3647
+ const instruction = value;
3648
+ return instruction?.type === "clearChangeListeners" /* clearChangeListeners */ && isNSChangeListenerTarget(instruction.target);
3643
3649
  }
3644
3650
  function isNSInstructionDebug(value) {
3645
3651
  const v = value;
3646
3652
  return v?.type === "debug" /* debug */ && ["log", "warning", "error", "assert"].includes(v.severity ?? "") && isNSPointer(v.message) && isNSTypeInfo(v.messageType) && (v.severity === "assert" ? isNSPointer(v.condition) : v.condition === void 0);
3647
3653
  }
3648
3654
  function isNSInstruction(value) {
3649
- return isNSInstructionDebug(value) || isNSInstructionVariable(value) || isNSInstructionIfBranch(value) || isNSInstructionReturn(value) || isNSInstructionThrow(value) || isNSInstructionAssign(value) || isNSInstructionCollectionCall(value) || isNSInstructionFunctionCall(value) || isNSInstructionConditionLoop(value) || isNSInstructionFor(value) || isNSInstructionForEach(value) || isNSInstructionBreak(value) || isNSInstructionContinue(value) || isNSInstructionSwitch(value) || isNSInstructionTry(value) || isNSInstructionChangeListener(value) || isNSInstructionAddActionListener(value) || isNSInstructionRemoveActionListener(value);
3655
+ return isNSInstructionDebug(value) || isNSInstructionVariable(value) || isNSInstructionIfBranch(value) || isNSInstructionReturn(value) || isNSInstructionThrow(value) || isNSInstructionAssign(value) || isNSInstructionCollectionCall(value) || isNSInstructionFunctionCall(value) || isNSInstructionConditionLoop(value) || isNSInstructionFor(value) || isNSInstructionForEach(value) || isNSInstructionBreak(value) || isNSInstructionContinue(value) || isNSInstructionSwitch(value) || isNSInstructionTry(value) || isNSInstructionChangeListener(value) || isNSInstructionClearChangeListeners(value) || isNSInstructionAddActionListener(value) || isNSInstructionRemoveActionListener(value);
3650
3656
  }
3651
3657
  function isNSInstructions(value) {
3652
3658
  return Array.isArray(value) && value.every(isNSInstruction);
@@ -13204,6 +13210,9 @@ function listenerWritesByOwner(state) {
13204
13210
  }
13205
13211
  return index.owners;
13206
13212
  }
13213
+ function changeListenerWriteEdit(write) {
13214
+ return write.operation === "clear" ? { kind: "replace", targets: [] } : { kind: write.operation, target: write.listener };
13215
+ }
13207
13216
 
13208
13217
  // ../src/runtime/neoscript/typed-aggregate-row-references.ts
13209
13218
  function visitTypedAggregateRowReferences(value, rawType, visitor) {
@@ -17032,33 +17041,41 @@ function finalizeConstructorAllocations(ctx, returnValue, ownsInvocationState, r
17032
17041
  const listenerEdges = /* @__PURE__ */ new Map();
17033
17042
  for (const write of state.writes) {
17034
17043
  if (!retainListenerReceivers || write.kind !== "changeListener") continue;
17044
+ const slotKey = JSON.stringify([write.ownerId, write.memberId]);
17045
+ if (write.operation === "clear") {
17046
+ listenerEdges.delete(slotKey);
17047
+ continue;
17048
+ }
17035
17049
  const key = JSON.stringify([
17036
- write.ownerId,
17037
- write.memberId,
17038
17050
  write.listener.memberId,
17039
17051
  write.listener.valueId
17040
17052
  ]);
17041
- if (write.operation === "remove") listenerEdges.delete(key);
17042
- else listenerEdges.set(key, write);
17053
+ let edges = listenerEdges.get(slotKey);
17054
+ if (write.operation === "remove") edges?.delete(key);
17055
+ else {
17056
+ if (edges === void 0) listenerEdges.set(slotKey, edges = /* @__PURE__ */ new Map());
17057
+ edges.set(key, write);
17058
+ }
17043
17059
  }
17044
17060
  const externalListenerReceivers = /* @__PURE__ */ new Set();
17045
- for (const write of listenerEdges.values()) {
17046
- if (write.listener.valueId === null) continue;
17047
- const receiverGroup = groupByRowId.get(write.listener.valueId);
17048
- if (receiverGroup === void 0) continue;
17049
- const ownerGroup = groupByRowId.get(write.ownerId);
17050
- if (ownerGroup === void 0) {
17051
- if (rowForCleanup(write.ownerId) !== null)
17052
- externalListenerReceivers.add(receiverGroup);
17053
- } else {
17054
- let dependencies = state.constructorGroupDependencies.get(ownerGroup);
17055
- if (dependencies === void 0) {
17056
- dependencies = /* @__PURE__ */ new Set();
17057
- state.constructorGroupDependencies.set(ownerGroup, dependencies);
17061
+ for (const edges of listenerEdges.values())
17062
+ for (const write of edges.values()) {
17063
+ if (write.listener.valueId === null) continue;
17064
+ const receiverGroup = groupByRowId.get(write.listener.valueId);
17065
+ if (receiverGroup === void 0) continue;
17066
+ const ownerGroup = groupByRowId.get(write.ownerId);
17067
+ if (ownerGroup === void 0) {
17068
+ if (rowForCleanup(write.ownerId) !== null)
17069
+ externalListenerReceivers.add(receiverGroup);
17070
+ } else {
17071
+ let dependencies = state.constructorGroupDependencies.get(ownerGroup);
17072
+ if (dependencies === void 0) {
17073
+ dependencies = /* @__PURE__ */ new Set();
17074
+ state.constructorGroupDependencies.set(ownerGroup, dependencies);
17075
+ }
17076
+ dependencies.add(receiverGroup);
17058
17077
  }
17059
- dependencies.add(receiverGroup);
17060
17078
  }
17061
- }
17062
17079
  const markConstructorGroupEscaped = (rootId) => {
17063
17080
  if (escapedRoots.has(rootId)) return;
17064
17081
  escapedRoots.add(rootId);
@@ -17255,7 +17272,7 @@ function finalizeConstructorAllocations(ctx, returnValue, ownsInvocationState, r
17255
17272
  const survivingWrites = state.writes.filter((write) => {
17256
17273
  if (write.kind === "changeListener") {
17257
17274
  if (collectedIds.has(write.ownerId)) return false;
17258
- return write.listener.valueId === null || !collectedIds.has(write.listener.valueId);
17275
+ return write.operation === "clear" || write.listener.valueId === null || !collectedIds.has(write.listener.valueId);
17259
17276
  }
17260
17277
  const destination = write.resolvedDestination;
17261
17278
  const destinationId = destination !== void 0 && "containerValueId" in destination ? destination.containerValueId : void 0;
@@ -18009,7 +18026,8 @@ function evalInstructions(instructions, scope, ctx, options) {
18009
18026
  break;
18010
18027
  }
18011
18028
  case "addChangeListener" /* addChangeListener */:
18012
- case "removeChangeListener" /* removeChangeListener */: {
18029
+ case "removeChangeListener" /* removeChangeListener */:
18030
+ case "clearChangeListeners" /* clearChangeListeners */: {
18013
18031
  evalChangeListenerInstruction(ins, scope, ctx, options);
18014
18032
  break;
18015
18033
  }
@@ -18430,6 +18448,18 @@ function evalChangeListenerInstruction(ins, scope, ctx, options) {
18430
18448
  throw new NSGetterRuntimeError(
18431
18449
  `OnChanged member '${ins.target.memberId}' is not an observable member or getter of owner '${owner.id}'.`
18432
18450
  );
18451
+ if (ins.type === "clearChangeListeners" /* clearChangeListeners */) {
18452
+ if (getter !== null) clearGetterSubscription(getter, ctx);
18453
+ else
18454
+ options.writes.push({
18455
+ kind: "changeListener",
18456
+ target: ins.target,
18457
+ ownerId: owner.id,
18458
+ memberId: slot.memberId,
18459
+ operation: "clear"
18460
+ });
18461
+ return;
18462
+ }
18433
18463
  const listener = evalPointer(ins.listener, scope, ctx);
18434
18464
  if (!isMemberDelegateTarget(listener))
18435
18465
  throw new NSGetterRuntimeError(
@@ -18486,6 +18516,17 @@ function editGetterSubscription(slot, target, add, ctx) {
18486
18516
  }
18487
18517
  subscription.targets.push(target);
18488
18518
  }
18519
+ function clearGetterSubscription(slot, ctx) {
18520
+ if ((ctx.__executionState?.constructionStack.length ?? 0) > 0)
18521
+ throw new NSGetterRuntimeError(
18522
+ "A getter subscription lives only for the play session, so a constructor can't clear one. Clear from a function."
18523
+ );
18524
+ const subscriptions = ctx.__getterSubscriptions;
18525
+ const subscription = subscriptions?.get(slot.owner.id, slot.memberId);
18526
+ if (subscriptions === void 0 || subscription === void 0) return;
18527
+ subscription.targets.length = 0;
18528
+ subscriptions.drop(subscription);
18529
+ }
18489
18530
  function dispatchGetterSubscriptions(ctx) {
18490
18531
  const subscriptions = ctx.__getterSubscriptions;
18491
18532
  if (subscriptions === void 0) return;
@@ -26802,7 +26843,7 @@ function cloneClassValueGraph(receiver, expectedClassId, ctx) {
26802
26843
  const edit = editChangeListenerSet(
26803
26844
  entry?.[write.memberId],
26804
26845
  [],
26805
- { kind: write.operation, target: write.listener },
26846
+ changeListenerWriteEdit(write),
26806
26847
  { inheritedComplete: false }
26807
26848
  );
26808
26849
  if (!edit.changed) continue;
@@ -34329,6 +34370,7 @@ var WRITE_TARGET_DESCRIPTIONS = {
34329
34370
  projectValue: "a stored project value",
34330
34371
  unknown: "something other than `this` or a local"
34331
34372
  };
34373
+ var ON_CHANGED_OPERATIONS_ERROR = "OnChanged supports only '+=', '-=', and Clear().";
34332
34374
  var FALLTHROUGH_LABELS = {
34333
34375
  function: "NSFunction",
34334
34376
  getter: "getter",
@@ -35671,6 +35713,12 @@ var StrictNeoScriptResolver = class _StrictNeoScriptResolver {
35671
35713
  this.resolveExpression(statement.expr, scope);
35672
35714
  const debug = this.resolveDebugStatement(statement.expr, scope);
35673
35715
  if (debug) return debug;
35716
+ const listenerClear = this.resolveChangeListenerClear(
35717
+ statement.expr,
35718
+ scope,
35719
+ statement.pos
35720
+ );
35721
+ if (listenerClear) return listenerClear;
35674
35722
  const actionClear = this.resolveActionClear(
35675
35723
  statement.expr,
35676
35724
  scope,
@@ -36031,8 +36079,20 @@ var StrictNeoScriptResolver = class _StrictNeoScriptResolver {
36031
36079
  const observed = this.resolveExpression(statement.target.receiver, scope);
36032
36080
  const observedType = neoScriptMemberLookupType(observed.type);
36033
36081
  const declared = observedType.kind === "named" && this.project.typeById.get(observedType.typeId)?.members.some((member) => member.name === "OnChanged");
36034
- if (!declared)
36035
- return this.resolveChangeSubscription(statement, observed, scope);
36082
+ if (!declared) {
36083
+ if (statement.op !== "+=" && statement.op !== "-=")
36084
+ throw new CompileError(ON_CHANGED_OPERATIONS_ERROR, statement.pos);
36085
+ return this.resolveChangeListener(
36086
+ statement.target,
36087
+ observed,
36088
+ scope,
36089
+ statement.pos,
36090
+ {
36091
+ kind: statement.op === "+=" ? "addChangeListener" /* AddChangeListener */ : "removeChangeListener" /* RemoveChangeListener */,
36092
+ handler: statement.value
36093
+ }
36094
+ );
36095
+ }
36036
36096
  }
36037
36097
  const local = statement.target.kind === "ident" ? scope.lookup(statement.target.name) : null;
36038
36098
  if (local?.readonlyBinding) {
@@ -36216,158 +36276,142 @@ var StrictNeoScriptResolver = class _StrictNeoScriptResolver {
36216
36276
  pos
36217
36277
  );
36218
36278
  }
36219
- resolveChangeSubscription(statement, observed, scope) {
36220
- if (statement.op !== "+=" && statement.op !== "-=") {
36221
- throw new CompileError(
36222
- "OnChanged supports only '+=' and '-=' subscriptions.",
36223
- statement.pos
36224
- );
36225
- }
36226
- if (statement.target.kind !== "member") {
36279
+ /** `X.M.OnChanged.Clear()` empties M's listener set on X (P70, P106). */
36280
+ resolveChangeListenerClear(expression, scope, pos) {
36281
+ if (expression.kind !== "call" || expression.callee.kind !== "member" || expression.callee.name !== "Clear" || expression.callee.receiver.kind !== "member" || expression.callee.receiver.name !== "OnChanged")
36282
+ return null;
36283
+ const onChanged = expression.callee.receiver;
36284
+ const observed = this.resolveExpression(onChanged.receiver, scope);
36285
+ const observedType = neoScriptMemberLookupType(observed.type);
36286
+ if (observedType.kind === "named" && this.project.typeById.get(observedType.typeId)?.members.some((member) => member.name === "OnChanged"))
36287
+ return null;
36288
+ if (expression.callee.optional)
36227
36289
  throw new CompileError(
36228
- "OnChanged requires a declared member slot.",
36229
- statement.pos
36290
+ "OnChanged is never null, so optional-chained Clear is not supported.",
36291
+ pos
36230
36292
  );
36231
- }
36232
- if (statement.target.optional) {
36293
+ requireArgCount("Clear", expression.args, 0, pos);
36294
+ return this.resolveChangeListener(onChanged, observed, scope, pos, {
36295
+ kind: "clearChangeListeners" /* ClearChangeListeners */
36296
+ });
36297
+ }
36298
+ resolveChangeListener(onChanged, observed, scope, pos, edit) {
36299
+ if (onChanged.optional) {
36233
36300
  throw new CompileError(
36234
36301
  "OnChanged cannot use an optional subscription target.",
36235
- statement.pos
36302
+ pos
36236
36303
  );
36237
36304
  }
36238
36305
  if (observed.optionalChainType !== void 0) {
36239
36306
  throw new CompileError(
36240
36307
  "OnChanged cannot subscribe through optional chaining; narrow the receiver first.",
36241
- statement.pos
36308
+ pos
36242
36309
  );
36243
36310
  }
36244
36311
  const member = observed.symbol;
36245
36312
  if (!member)
36246
36313
  throw new CompileError(
36247
36314
  "OnChanged requires a declared member, not a local variable.",
36248
- statement.pos
36315
+ pos
36249
36316
  );
36250
36317
  if (observed.pointer.type === "staticMember" /* StaticMember */ || observed.pointer.type === "callGetter" /* CallGetter */ && observed.pointer.receiver.kind === "static") {
36251
- const access = statement.target.receiver;
36318
+ const access = onChanged.receiver;
36252
36319
  const name = access.kind === "member" && access.receiver.kind === "ident" ? `${access.receiver.name}.${member.name}` : member.name;
36253
36320
  throw new CompileError(
36254
36321
  `OnChanged requires an instance member. '${name}' is static.`,
36255
- statement.pos
36322
+ pos
36256
36323
  );
36257
36324
  }
36258
36325
  if (observed.pointer.type === "callGetter" /* CallGetter */)
36259
- return this.resolveGetterSubscription(
36260
- statement,
36261
- statement.target.receiver,
36326
+ return this.resolveGetterChangeListener(
36327
+ onChanged.receiver,
36262
36328
  observed,
36263
36329
  observed.pointer,
36264
36330
  member,
36265
- scope
36331
+ scope,
36332
+ pos,
36333
+ edit
36266
36334
  );
36267
36335
  if (observed.pointer.type !== "keyOf" /* KeyOf */)
36268
- throw new CompileError(
36269
- "OnChanged requires a declared member slot.",
36270
- statement.pos
36271
- );
36336
+ throw new CompileError("OnChanged requires a declared member slot.", pos);
36272
36337
  if (!observed.pointer.memberId)
36273
36338
  throw new CompileError(
36274
36339
  "OnChanged cannot observe a collection entry.",
36275
- statement.pos
36340
+ pos
36276
36341
  );
36277
36342
  if (member.computed) {
36278
36343
  throw new CompileError(
36279
36344
  "OnChanged cannot observe a computed member.",
36280
- statement.pos
36345
+ pos
36281
36346
  );
36282
36347
  }
36283
36348
  if (member.readonlyDeclaration) {
36284
36349
  throw new CompileError(
36285
36350
  "OnChanged cannot observe a readonly declaration.",
36286
- statement.pos
36351
+ pos
36287
36352
  );
36288
36353
  }
36289
36354
  if (observed.type.kind === "delegate" || observed.type.kind === "action") {
36290
36355
  throw new CompileError(
36291
36356
  "OnChanged cannot observe a callable member.",
36292
- statement.pos
36357
+ pos
36293
36358
  );
36294
36359
  }
36295
36360
  const writability = observed.userData ? "runtime" /* Runtime */ : observed.writability;
36296
36361
  if (writability !== "save" /* Save */ && writability !== "session" /* Session */ && writability !== "runtime" /* Runtime */ && writability !== "local" /* Local */) {
36297
36362
  throw new CompileError(
36298
36363
  "OnChanged requires a member whose storage is writable at gameplay runtime.",
36299
- statement.pos
36364
+ pos
36300
36365
  );
36301
36366
  }
36302
36367
  this.validateConstructorWriteTarget(
36303
36368
  observed.pointer,
36304
- "subscribe to",
36305
- statement.pos,
36369
+ edit.kind === "clearChangeListeners" /* ClearChangeListeners */ ? "clear listeners on" : "subscribe to",
36370
+ pos,
36306
36371
  scope,
36307
36372
  false
36308
36373
  );
36309
- if (!statement.value) {
36310
- throw new CompileError(
36311
- "OnChanged subscription requires a handler member.",
36312
- statement.pos
36313
- );
36314
- }
36315
36374
  const storageType = this.assignmentStorageType(
36316
- statement.target.receiver,
36375
+ onChanged.receiver,
36317
36376
  observed,
36318
36377
  null,
36319
36378
  scope
36320
36379
  );
36321
- const expected = {
36322
- kind: "action",
36323
- parameterTypes: [storageType]
36324
- };
36325
- const listener = this.resolveActionListener(
36326
- statement.value,
36327
- expected,
36328
- scope,
36329
- true,
36330
- true
36331
- );
36332
- return {
36333
- type: statement.op === "+=" ? "addChangeListener" /* AddChangeListener */ : "removeChangeListener" /* RemoveChangeListener */,
36334
- target: {
36380
+ return this.changeListenerInstruction(
36381
+ edit,
36382
+ {
36335
36383
  owner: observed.pointer.keyOf.pointer,
36336
36384
  memberId: member.canonicalMemberId ?? observed.pointer.memberId,
36337
36385
  typeInfo: toWireType(storageType, this.project),
36338
36386
  writability
36339
36387
  },
36340
- listener
36341
- };
36388
+ storageType,
36389
+ scope,
36390
+ pos
36391
+ );
36342
36392
  }
36343
36393
  /**
36344
36394
  * P106. A getter subscription lives only in the running client's memory:
36345
36395
  * nothing persists it, so construction replay would silently drop one.
36346
36396
  */
36347
- resolveGetterSubscription(statement, getterRead, observed, pointer, member, scope) {
36397
+ resolveGetterChangeListener(getterRead, observed, pointer, member, scope, pos, edit) {
36348
36398
  if (pointer.dispatch === "base") {
36349
36399
  throw new CompileError(
36350
36400
  `OnChanged can't observe a base getter. Subscribe to '${member.name}' on the instance.`,
36351
- statement.pos
36401
+ pos
36352
36402
  );
36353
36403
  }
36354
36404
  if (pointer.receiver.kind !== "instance") {
36355
36405
  throw new CompileError(
36356
36406
  `OnChanged requires an instance receiver for getter '${member.name}'.`,
36357
- statement.pos
36407
+ pos
36358
36408
  );
36359
36409
  }
36360
36410
  const kind = this.context.kind;
36361
36411
  if (isNeoScriptConstructionDocumentKind(kind) || kind === "migration") {
36362
36412
  throw new CompileError(
36363
- `A getter subscription lives only for the play session, so ${dialogueBodyKindLabel(kind)} can't make one. Subscribe from a function.`,
36364
- statement.pos
36365
- );
36366
- }
36367
- if (!statement.value) {
36368
- throw new CompileError(
36369
- "OnChanged subscription requires a handler member.",
36370
- statement.pos
36413
+ edit.kind === "clearChangeListeners" /* ClearChangeListeners */ ? `A getter subscription lives only for the play session, so ${dialogueBodyKindLabel(kind)} can't clear one. Clear from a function.` : `A getter subscription lives only for the play session, so ${dialogueBodyKindLabel(kind)} can't make one. Subscribe from a function.`,
36414
+ pos
36371
36415
  );
36372
36416
  }
36373
36417
  const getterType = this.assignmentStorageType(
@@ -36376,23 +36420,36 @@ var StrictNeoScriptResolver = class _StrictNeoScriptResolver {
36376
36420
  null,
36377
36421
  scope
36378
36422
  );
36379
- const listener = this.resolveActionListener(
36380
- statement.value,
36381
- { kind: "action", parameterTypes: [getterType] },
36382
- scope,
36383
- true,
36384
- true
36385
- );
36386
- return {
36387
- type: statement.op === "+=" ? "addChangeListener" /* AddChangeListener */ : "removeChangeListener" /* RemoveChangeListener */,
36388
- target: {
36423
+ return this.changeListenerInstruction(
36424
+ edit,
36425
+ {
36389
36426
  owner: pointer.receiver.pointer,
36390
36427
  memberId: member.canonicalMemberId ?? pointer.memberId,
36391
36428
  typeInfo: toWireType(getterType, this.project),
36392
36429
  writability: "session" /* Session */
36393
36430
  },
36394
- listener
36395
- };
36431
+ getterType,
36432
+ scope,
36433
+ pos
36434
+ );
36435
+ }
36436
+ changeListenerInstruction(edit, target, observedType, scope, pos) {
36437
+ if (edit.kind === "clearChangeListeners" /* ClearChangeListeners */)
36438
+ return { type: edit.kind, target };
36439
+ if (!edit.handler) {
36440
+ throw new CompileError(
36441
+ "OnChanged subscription requires a handler member.",
36442
+ pos
36443
+ );
36444
+ }
36445
+ const listener = this.resolveActionListener(
36446
+ edit.handler,
36447
+ { kind: "action", parameterTypes: [observedType] },
36448
+ scope,
36449
+ true,
36450
+ true
36451
+ );
36452
+ return { type: edit.kind, target, listener };
36396
36453
  }
36397
36454
  /**
36398
36455
  * P62 §3.2. `+=` / `-=` on an NSAction member are subscription operators,
@@ -38510,6 +38567,8 @@ var StrictNeoScriptResolver = class _StrictNeoScriptResolver {
38510
38567
  entryUserData
38511
38568
  };
38512
38569
  }
38570
+ if (name === "OnChanged")
38571
+ throw new CompileError(ON_CHANGED_OPERATIONS_ERROR, pos);
38513
38572
  throw new CompileError(
38514
38573
  `Cannot read member '${name}' on ${this.describe(receiver.type)}`,
38515
38574
  pos
@@ -55688,7 +55747,7 @@ var SPECIALIZED_RELATION_KINDS = /* @__PURE__ */ new Set([
55688
55747
 
55689
55748
  // ../packages/manifest/schema-contract/registry.mjs
55690
55749
  var PROJECT_SCHEMA_CONTRACT = Object.freeze({
55691
- cliVersion: "0.74.0",
55750
+ cliVersion: "0.75.0",
55692
55751
  projectFileUploadBatchSize: 32,
55693
55752
  documentRecords: {
55694
55753
  member: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.74.0",
3
+ "version": "0.75.0",
4
4
  "description": "Neo Compose native project-source CLI with bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ description: >-
9
9
  `@neocompose/cli` or `node cli/bin/neo.mjs` in the neo-compose repository.
10
10
  ---
11
11
 
12
- <!-- reviewed-through-cli: 0.74.0 -->
12
+ <!-- reviewed-through-cli: 0.75.0 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -118,7 +118,7 @@ wrappers.
118
118
  The marker near the top of `SKILL.md` must exactly match the package version:
119
119
 
120
120
  ```html
121
- <!-- reviewed-through-cli: 0.74.0 -->
121
+ <!-- reviewed-through-cli: 0.75.0 -->
122
122
  ```
123
123
 
124
124
  The quoted version above is checked too, so this instruction cannot go stale
@@ -557,7 +557,8 @@ receiver.Handler`; use `-=` to remove the same receiver/handler pair. Handlers
557
557
  are noncapturing, return void, and accept the field's type. Bindings to Session
558
558
  receivers stay Session-only. Gameplay hosts dispatch after the write batch.
559
559
  Authoring, replay, migration, and dialogue dry-run hosts retain wiring without
560
- invoking callbacks.
560
+ invoking callbacks. `owner.Field.OnChanged.Clear()` removes every handler,
561
+ including `@listeners` defaults, in both Save and Session wiring.
561
562
 
562
563
  Instance getters take the same syntax, for example
563
564
  `this._quantity.Value.OnChanged += this.QuantityChanged`. A getter
@@ -566,7 +567,8 @@ function, effect, lifecycle hook, or dialogue action, never a constructor,
566
567
  initializer, or migration. Subscribing reads the getter once. The SDK calls the
567
568
  handler with a fresh result whenever something that read depended on changes,
568
569
  even if the result is equal, and several changes before the next read call it
569
- once. Static getters, `base.Value`, and `@listeners` can't observe a getter.
570
+ once. `Clear()` ends the subscription, and a constructor can't call it on a
571
+ getter. Static getters, `base.Value`, and `@listeners` can't observe a getter.
570
572
  `neo test` dispatches getter subscriptions the same way, treating the test body
571
573
  as the host: handlers run after each of its statements, so the next statement
572
574
  sees their effects. A getter that reads through `Clone()` or a grid query can