sandstone 1.2.1 → 1.2.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sandstone",
3
3
  "description": "Sandstone, a Typescript library for Minecraft datapacks & resource packs.",
4
- "version": "1.2.1",
4
+ "version": "1.2.2",
5
5
  "contributors": [
6
6
  {
7
7
  "name": "TheMrZZ - Florian ERNST",
@@ -20,7 +20,14 @@ export class CommandConditionNode extends SingleConditionNode {
20
20
  const store = sandstoneCore.pack.commands.execute.store[type]
21
21
  this.variable = sandstoneCore.pack.Variable(undefined, 'condition')
22
22
 
23
+ // Snapshot before/after so commands the callback commits land in `preNodes` instead of
24
+ // staying on the user MCFunction body, where they'd be stranded when visitor extraction
25
+ // moves the surrounding condition chain elsewhere.
26
+ const mcFunction = sandstoneCore.getCurrentMCFunctionOrThrow()
27
+ const before = mcFunction.body.length
23
28
  command(store(this.variable))
29
+ const after = mcFunction.body.length
30
+ this.preNodes = mcFunction.body.splice(before, after - before)
24
31
  }
25
32
 
26
33
  getCondition(): unknown[] {
@@ -18,6 +18,15 @@ export type GenericConditionType = {
18
18
  }
19
19
 
20
20
  export abstract class ConditionNode extends Node {
21
+ /**
22
+ * Optional commands that must run before this condition's predicate is evaluated.
23
+ * Conditions that emit side-effect commands at construction time (e.g. `data.equals`
24
+ * building an `execute store result ...`) capture them here so the visitor pass can
25
+ * re-emit them at the correct location — inline before the IfNode, or inside the
26
+ * child MCFunction when AND/OR extracts. Pure predicates leave it unset.
27
+ */
28
+ preNodes?: Node[]
29
+
21
30
  abstract getValue: (negated?: boolean) => string
22
31
  }
23
32
 
@@ -1,5 +1,5 @@
1
1
  import type { NBTObject } from 'sandstone/arguments/nbt'
2
- import type { DataPointPickClass, SandstoneCore } from 'sandstone/core'
2
+ import type { DataPointPickClass, Node, SandstoneCore } from 'sandstone/core'
3
3
  import type { DataPointClass, Score } from 'sandstone/variables'
4
4
  import { SingleConditionNode } from '../condition'
5
5
 
@@ -29,11 +29,19 @@ export class DataPointEqualsConditionNode extends SingleConditionNode {
29
29
  const { DataVariable, Variable, commands } = sandstoneCore.pack
30
30
  const { execute } = commands
31
31
 
32
+ // Snapshot the user MCFunction body length before/after the entire side-effecting
33
+ // sequence — both the `data modify ... set from <source>` emitted by DataVariable (which
34
+ // copies the source into the anonymous storage) AND the `execute store result ... set
35
+ // value` written by `execute.store.result().run()`. They belong together: the first
36
+ // loads the data, the second overwrites it with `value`, and the subsequent
37
+ // `if score matches 0..` only makes sense after both have run.
38
+ const mcFunction = sandstoneCore.getCurrentMCFunctionOrThrow()
39
+ const before = mcFunction.body.length
32
40
  const anon = DataVariable(this.dataPoint)
33
-
34
41
  this.conditional = Variable()
35
-
36
42
  execute.store.result(this.conditional).run(() => anon.set(this.value as unknown as DataPointClass))
43
+ const after = mcFunction.body.length
44
+ this.preNodes = mcFunction.body.splice(before, after - before)
37
45
  }
38
46
 
39
47
  getValue = (negated?: boolean) =>
package/src/pack/pack.ts CHANGED
@@ -469,11 +469,12 @@ export class SandstonePack {
469
469
  }
470
470
 
471
471
  setupLantern = () => {
472
- const loadStatus = this.Objective.create('load.status', 'dummy', undefined, { useDefaultNamespace: false })
472
+ const loadStatus = this.Objective.create('load.status', 'dummy', undefined, { useDefaultNamespace: false, alreadyExists: true })
473
473
 
474
474
  const privateInit = this.Tag('function', 'load:_private/init', [
475
475
  this.MCFunction('load:_private/init', () => {
476
476
  this.commands.comment('Reset scoreboards so packs can set values accurate for current load.')
477
+ this.commands.scoreboard.objectives.add(loadStatus.name, 'dummy')
477
478
  loadStatus.reset()
478
479
  }),
479
480
  ])
@@ -1,68 +1,342 @@
1
- import { ScoreboardCommandNode } from '../../commands'
2
- import type { Node } from '../../core'
3
- import { AndNode, type ConditionNode, IfNode, NotNode, OrNode } from '../../flow'
1
+ import { ExecuteCommandNode, type SubCommand, ReturnCommandNode, ScoreboardCommandNode } from '../../commands'
2
+ import { MCFunctionClass, type MCFunctionNode, type Node, type SandstoneCore } from '../../core'
3
+ import type { Score } from '../../variables'
4
+ import { AndNode, ConditionNode, IfNode, NotNode, OrNode } from '../../flow'
4
5
  import { GenericSandstoneVisitor } from './visitor'
5
6
 
7
+ /**
8
+ * Condition node whose `getValue()` returns `"if function <name>"`.
9
+ * Used to make an IfNode's existing `IfElseTransformationVisitor` emit
10
+ * `execute if function <child> run <body>`, which succeeds exactly when the
11
+ * child MCFunction returns a positive value (e.g. via an early `return 1`).
12
+ */
13
+ class FunctionReturnConditionNode extends ConditionNode {
14
+ constructor(
15
+ public sandstoneCore: SandstoneCore,
16
+ public functionName: string,
17
+ ) {
18
+ super(sandstoneCore)
19
+ }
20
+
21
+ getValue = () => `if function ${this.functionName}`
22
+ }
23
+
6
24
  export class OrTransformationVisitor extends GenericSandstoneVisitor {
7
25
  visitIfNode = (node_: IfNode) => {
8
- const { preNodes, conditionNode } = this.parseConditionNode(node_.condition)
26
+ const { preNodes, conditionNode } = this.parseConditionNode(node_.condition, node_.parentMCFunction)
9
27
  node_.condition = conditionNode
10
28
  return [...preNodes, this.genericVisit(node_)]
11
29
  }
12
30
 
13
- parseConditionNode = (node: ConditionNode): { preNodes: Node[]; conditionNode: ConditionNode } => {
31
+ parseConditionNode = (node: ConditionNode, parentMCFunction?: MCFunctionNode): { preNodes: Node[]; conditionNode: ConditionNode } => {
14
32
  if (node instanceof OrNode) {
15
- return this.parseOrNode(node)
33
+ return this.parseOrNode(node, parentMCFunction)
16
34
  }
17
35
  if (node instanceof AndNode) {
18
- return this.parseAndNode(node)
36
+ return this.parseAndNode(node, parentMCFunction)
19
37
  }
20
38
  if (node instanceof NotNode) {
21
- return this.parseNotNode(node)
39
+ return this.parseNotNode(node, parentMCFunction)
22
40
  }
23
41
 
24
- // Default case, just return the node as is
25
- return { preNodes: [], conditionNode: node }
42
+ // `preNodes` is optional on ConditionNode. Side-effecting conditions (those that
43
+ // commit commands at construction, e.g. `data.equals`, `Label.equals`, any
44
+ // `CommandCondition`) populate it; pure predicates leave it unset.
45
+ return { preNodes: [...(node.preNodes ?? [])], conditionNode: node }
26
46
  }
27
- parseOrNode = (node: OrNode): { preNodes: Node[]; conditionNode: ConditionNode } => {
28
- const variable = this.core.pack.Variable(undefined, 'condition')
29
47
 
30
- const conditionNode = variable.equalTo(1)._toMinecraftCondition()
48
+ parseOrNode = (node: OrNode, parentMCFunction?: MCFunctionNode): { preNodes: Node[]; conditionNode: ConditionNode } => {
49
+ // Single branch: collapse through so the IfNode behaves as if it had the inner condition.
50
+ if (node.conditions.length === 1) {
51
+ return this.parseConditionNode(node.conditions[0], parentMCFunction)
52
+ }
53
+
54
+ // Multiple branches: extract to a child MCFunction with execute-if-return-1 short-circuit.
55
+ // Inside the child: `execute if <c1> run return 1`, `execute if <c2> run return 1`, ..., `return 0`.
56
+ // The first matching branch returns 1 and exits the function immediately; the rest are skipped.
57
+ // The caller uses `execute if function <child>` to test whether any branch matched.
58
+ const parentName = parentMCFunction?.resource.name ?? '__sandstone'
59
+ const orMCFunction = new MCFunctionClass(
60
+ this.core,
61
+ `${parentName}/or_check`,
62
+ {
63
+ addToSandstoneCore: true,
64
+ creator: 'sandstone',
65
+ onConflict: 'rename',
66
+ },
67
+ )
31
68
 
32
- const realPreNodes: Node[] = [new ScoreboardCommandNode(this.pack, 'players', 'reset', variable)]
33
- const orExecuteNodes = node.conditions.map((condition) => {
34
- const { preNodes, conditionNode: parsedConditionNode } = this.parseConditionNode(condition)
35
- realPreNodes.push(...preNodes)
69
+ this.core.enterMCFunction(orMCFunction)
36
70
 
37
- const ifNode = new IfNode(this.core, parsedConditionNode)
38
- ifNode.body = [new ScoreboardCommandNode(this.pack, 'players', 'set', variable, 1)]
39
- return ifNode
40
- })
71
+ try {
72
+ for (const condition of node.conditions) {
73
+ this.pushCheckBranch(orMCFunction.node, this.parseConditionNode(condition, orMCFunction.node), false, new ReturnCommandNode(this.pack, [1]))
74
+ }
75
+
76
+ // Default return: no branch matched.
77
+ orMCFunction.node.body.push(new ReturnCommandNode(this.pack, [0]))
78
+ } finally {
79
+ this.core.exitMCFunction()
80
+ }
41
81
 
42
82
  return {
43
- preNodes: [...realPreNodes, ...orExecuteNodes],
44
- conditionNode,
83
+ preNodes: [],
84
+ conditionNode: new FunctionReturnConditionNode(this.core, orMCFunction.name),
45
85
  }
46
86
  }
47
- parseAndNode = (node: AndNode): { preNodes: Node[]; conditionNode: ConditionNode } => {
48
- const finalPreNodes: Node[] = []
49
-
50
- const conditionNode = new AndNode(
51
- node.sandstoneCore,
52
- node.conditions.map((condition) => {
53
- const { preNodes, conditionNode } = this.parseConditionNode(condition)
54
- finalPreNodes.push(...preNodes)
55
- return conditionNode
56
- }),
87
+
88
+ parseAndNode = (node: AndNode, parentMCFunction?: MCFunctionNode): { preNodes: Node[]; conditionNode: ConditionNode } => {
89
+ // Single branch: collapse through so the IfNode behaves as if it had the inner condition.
90
+ if (node.conditions.length === 1) {
91
+ return this.parseConditionNode(node.conditions[0], parentMCFunction)
92
+ }
93
+
94
+ // Parse every condition to learn whether it carries commands (preNodes) that must run
95
+ // before the predicate is evaluated. Pure-predicate conditions chain cleanly inside a
96
+ // single execute — those are batched. Command-needing conditions force the AND into a
97
+ // child MCFunction so commands can run inline before their check.
98
+ const parsed = node.conditions.map((condition) => this.parseConditionNode(condition, parentMCFunction))
99
+ const anyHasCommands = parsed.some((p) => (p.preNodes ?? []).length > 0)
100
+
101
+ if (!anyHasCommands) {
102
+ // Inline: pure conditions chain via the existing AndNode behavior. Order is preserved
103
+ // by `Array.prototype.map`. No child function needed.
104
+ const finalPreNodes: Node[] = []
105
+ const conditionNode = new AndNode(
106
+ node.sandstoneCore,
107
+ parsed.map((p) => {
108
+ finalPreNodes.push(...p.preNodes)
109
+ return p.conditionNode
110
+ }),
111
+ )
112
+ return { preNodes: finalPreNodes, conditionNode }
113
+ }
114
+
115
+ // At least one condition needs to run commands. Extract to a child MCFunction:
116
+ // - Pure-predicate runs are batched into `execute <pos-c1> <pos-c2> ... run return 1`
117
+ // (positive form, NOT flipped — chain fires `return 1` only when the whole batch
118
+ // holds; otherwise we fall through to the next check).
119
+ // - Command-needing conditions emit their stashed side-effects inline, then a flipped
120
+ // `execute if <failure-form> run return 0` so the function returns 0 as soon as a
121
+ // side-effecting check fails to hold.
122
+ // - Default `return 0` reaches the end only when no command-needing check failed and
123
+ // no pure batch all-held; the function returns 1 only via a pure-batch success path,
124
+ // signalled to the caller by `execute if function <and>`.
125
+ const parentName = parentMCFunction?.resource.name ?? '__sandstone'
126
+ const andMCFunction = new MCFunctionClass(
127
+ this.core,
128
+ `${parentName}/and_check`,
129
+ {
130
+ addToSandstoneCore: true,
131
+ creator: 'sandstone',
132
+ onConflict: 'rename',
133
+ },
57
134
  )
58
135
 
136
+ this.core.enterMCFunction(andMCFunction)
137
+
138
+ try {
139
+ let batch: { preNodes: Node[][]; conditionNodes: ConditionNode[] } = { preNodes: [], conditionNodes: [] }
140
+
141
+ const flushBatch = () => {
142
+ if (batch.conditionNodes.length === 0 && batch.preNodes.every((pn) => pn.length === 0)) {
143
+ batch = { preNodes: [], conditionNodes: [] }
144
+ return
145
+ }
146
+ // Per-batch tracker-var pattern: reset the tracker, run the chained execute to set
147
+ // the tracker to 1 only when the entire batch holds, then check the tracker and
148
+ // `return 0` if any condition failed (var != 1). Each batch uses its own tracker var
149
+ // so concurrent/sequential batches stay independent.
150
+ const tracker = this.core.pack.Variable(undefined, 'and_batch')
151
+ this.pushBatchedCheckBranch(
152
+ andMCFunction.node,
153
+ batch.preNodes.flat(),
154
+ batch.conditionNodes,
155
+ tracker,
156
+ )
157
+ batch = { preNodes: [], conditionNodes: [] }
158
+ }
159
+
160
+ for (const p of parsed) {
161
+ if (p.preNodes.length === 0) {
162
+ // Pure predicate: extend the current batch (still in declared order).
163
+ batch.preNodes.push(p.preNodes)
164
+ batch.conditionNodes.push(p.conditionNode)
165
+ } else {
166
+ // Command-needing condition: flush any pending batch first (preserving order),
167
+ // then emit this condition's commands followed by its own fail-fast check.
168
+ flushBatch()
169
+ this.pushFailFastBranch(
170
+ andMCFunction.node,
171
+ p,
172
+ new ReturnCommandNode(this.pack, [0]),
173
+ )
174
+ }
175
+ }
176
+ flushBatch()
177
+
178
+ // Default return: every check ran without success.
179
+ andMCFunction.node.body.push(new ReturnCommandNode(this.pack, [1]))
180
+ } finally {
181
+ this.core.exitMCFunction()
182
+ }
183
+
59
184
  return {
60
- preNodes: finalPreNodes,
61
- conditionNode,
185
+ preNodes: [],
186
+ conditionNode: new FunctionReturnConditionNode(this.core, andMCFunction.name),
62
187
  }
63
188
  }
64
- parseNotNode = (node: NotNode): { preNodes: Node[]; conditionNode: ConditionNode } => {
65
- const { preNodes, conditionNode } = this.parseConditionNode(node.condition)
189
+
190
+ /**
191
+ * Pushes a batched check into `targetMCFunction.body` using the tracker-var pattern:
192
+ * <preNodes...>
193
+ * scoreboard players reset <tracker>
194
+ * execute <if|unless> <c1> <if|unless> <c2> ... run scoreboard players set <tracker> 1
195
+ * execute unless score <tracker> matches 1 run return 0
196
+ *
197
+ * Consecutive pure-predicate conditions from the AND list coalesce into a single chained
198
+ * execute that sets the tracker to 1 only when the entire batch holds. The trailing
199
+ * fail-check returns 0 from the child MCFunction when any condition failed (var != 1).
200
+ * Using a tracker var avoids the "execute-chain only fires on ALL-pass" limitation of
201
+ * pure MC chains while keeping the body tight.
202
+ */
203
+ private pushBatchedCheckBranch = (
204
+ targetMCFunction: MCFunctionNode,
205
+ preNodes: Node[],
206
+ conditionNodes: ConditionNode[],
207
+ tracker: Score,
208
+ ) => {
209
+ for (const n of preNodes) {
210
+ targetMCFunction.body.push(n)
211
+ }
212
+
213
+ targetMCFunction.body.push(new ScoreboardCommandNode(this.pack, 'players', 'reset', tracker))
214
+
215
+ const args: SubCommand[] = []
216
+ for (const c of conditionNodes) {
217
+ const condStr = c.getValue(false)
218
+ const match = condStr.match(/^(if|unless)\s+([\s\S]+)$/)
219
+ if (!match) {
220
+ throw new Error(`OrTransformationVisitor: unexpected condition getValue() shape: ${condStr}`)
221
+ }
222
+ const [, keyword, condBody] = match
223
+ args.push([keyword, condBody])
224
+ }
225
+
226
+ if (args.length === 0) {
227
+ // No conditions but preNodes ran — push an unconditional set so the trailing check
228
+ // doesn't fire on a stale tracker.
229
+ targetMCFunction.body.push(new ScoreboardCommandNode(this.pack, 'players', 'set', tracker, 1))
230
+ } else {
231
+ const executeNode = new ExecuteCommandNode(this.pack, args, {
232
+ isSingleExecute: true,
233
+ body: [],
234
+ })
235
+ targetMCFunction.enterContext(executeNode, false)
236
+ executeNode.body = [new ScoreboardCommandNode(this.pack, 'players', 'set', tracker, 1)]
237
+ targetMCFunction.body.push(executeNode)
238
+ targetMCFunction.exitContext()
239
+ }
240
+
241
+ const failCheckNode = new ExecuteCommandNode(
242
+ this.pack,
243
+ [['unless', `score ${tracker} matches 1`]],
244
+ {
245
+ isSingleExecute: true,
246
+ body: [],
247
+ },
248
+ )
249
+ targetMCFunction.enterContext(failCheckNode, false)
250
+ failCheckNode.body = [new ReturnCommandNode(this.pack, [0])]
251
+ targetMCFunction.body.push(failCheckNode)
252
+ targetMCFunction.exitContext()
253
+ }
254
+
255
+ /**
256
+ * Pushes a single check branch into `targetMCFunction.body`:
257
+ * <preNodes...> (side-effect commands that must run before checking)
258
+ * execute <if|unless> <condBody> run <bodyNode>
259
+ *
260
+ * When `negated` is false, the helper uses the condition's positive form
261
+ * (`getValue(false)`); when true, it uses the failure form (`getValue(true)`).
262
+ * For OR semantics: pass `negated=false` with `bodyNode = return 1` (exit on first match).
263
+ */
264
+ private pushCheckBranch = (
265
+ targetMCFunction: MCFunctionNode,
266
+ parsed: { preNodes: Node[]; conditionNode: ConditionNode },
267
+ negated: boolean,
268
+ bodyNode: Node,
269
+ ) => {
270
+ // Any nested preNodes (e.g. nested or/and reset+score commands, or the inner
271
+ // condition's side-effect commands) run inside the child function body in order.
272
+ for (const n of parsed.preNodes) {
273
+ targetMCFunction.body.push(n)
274
+ }
275
+
276
+ // Extract the chain keyword ("if" or "unless") and the condition body from `getValue()`.
277
+ // SingleConditionNode returns "if <body>"; NotNode-wrapped conditions return "unless <body>".
278
+ // AndNode already produces execute-chain strings; a nested FunctionReturnConditionNode
279
+ // returns "if function <name>" which fits the same shape.
280
+ const condStr = parsed.conditionNode.getValue(negated)
281
+ const match = condStr.match(/^(if|unless)\s+([\s\S]+)$/)
282
+ if (!match) {
283
+ throw new Error(`OrTransformationVisitor: unexpected condition getValue() shape: ${condStr}`)
284
+ }
285
+ const [, keyword, condBody] = match
286
+
287
+ // Construct ExecuteCommandNode for `execute <keyword> <condBody> run <bodyNode>`.
288
+ // The constructor's append() calls exitContext, so we first push an empty executeNode
289
+ // into the MCFunction context stack (addNode=false) so the exit succeeds.
290
+ const executeNode = new ExecuteCommandNode(this.pack, [[keyword, condBody]], {
291
+ isSingleExecute: true,
292
+ body: [],
293
+ })
294
+ targetMCFunction.enterContext(executeNode, false)
295
+ executeNode.body = [bodyNode]
296
+ targetMCFunction.body.push(executeNode)
297
+ targetMCFunction.exitContext()
298
+ }
299
+
300
+ /**
301
+ * Pushes a fail-fast check for an AND command-needing condition into `targetMCFunction.body`:
302
+ * <preNodes...>
303
+ * execute <if|unless> <condBody> run <bodyNode>
304
+ *
305
+ * Uses the condition's FAILURE form (`getValue(true)`) and flips the leading
306
+ * `if`/`unless` keyword so the execute fires when the condition fails to hold. The
307
+ * `bodyNode` (typically `return 0`) exits the child MCFunction immediately. When the
308
+ * condition holds, the chain doesn't fire and we fall through to the next check.
309
+ */
310
+ private pushFailFastBranch = (
311
+ targetMCFunction: MCFunctionNode,
312
+ parsed: { preNodes: Node[]; conditionNode: ConditionNode },
313
+ bodyNode: Node,
314
+ ) => {
315
+ for (const n of parsed.preNodes) {
316
+ targetMCFunction.body.push(n)
317
+ }
318
+
319
+ const condStr = parsed.conditionNode.getValue(true)
320
+ const match = condStr.match(/^(if|unless)\s+([\s\S]+)$/)
321
+ if (!match) {
322
+ throw new Error(`OrTransformationVisitor: unexpected condition getValue() shape: ${condStr}`)
323
+ }
324
+ // Flip the leading keyword so the execute fires on failure rather than success.
325
+ const flippedKeyword = match[1] === 'if' ? 'unless' : 'if'
326
+ const condBody = match[2]
327
+
328
+ const executeNode = new ExecuteCommandNode(this.pack, [[flippedKeyword, condBody]], {
329
+ isSingleExecute: true,
330
+ body: [],
331
+ })
332
+ targetMCFunction.enterContext(executeNode, false)
333
+ executeNode.body = [bodyNode]
334
+ targetMCFunction.body.push(executeNode)
335
+ targetMCFunction.exitContext()
336
+ }
337
+
338
+ parseNotNode = (node: NotNode, parentMCFunction?: MCFunctionNode): { preNodes: Node[]; conditionNode: ConditionNode } => {
339
+ const { preNodes, conditionNode } = this.parseConditionNode(node.condition, parentMCFunction)
66
340
  return {
67
341
  preNodes,
68
342
  conditionNode: new NotNode(node.sandstoneCore, conditionNode),
@@ -1,4 +1,5 @@
1
1
  import type { JSONTextComponent } from 'sandstone/arguments/jsonTextComponent'
2
+ import type { Node } from 'sandstone/core'
2
3
  import type { ConditionNode } from '../flow'
3
4
  import type { SelectorClass } from './Selector'
4
5
  import type { NBTSerializable } from 'sandstone/arguments'
@@ -24,6 +25,15 @@ export class ConditionClass {
24
25
  */
25
26
  declare readonly __conditionClassBrand: true
26
27
 
28
+ /**
29
+ * Optional commands that must run before this condition's predicate is evaluated.
30
+ * Conditions that emit side-effect commands at construction (e.g. `data.equals`
31
+ * building `execute store result ...`) capture them here so the visitor pass can
32
+ * re-emit them at the right location — inline before the IfNode, or inside the
33
+ * child MCFunction when AND/OR extracts. Optional; pure predicates leave it unset.
34
+ */
35
+ preNodes?: Node[]
36
+
27
37
  /**
28
38
  * @internal
29
39
  */