redscript-mc 1.2.21 → 1.2.25

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.
Files changed (126) hide show
  1. package/.github/workflows/publish-extension-on-ci.yml +100 -0
  2. package/dist/__tests__/entity-types.test.d.ts +1 -0
  3. package/dist/__tests__/entity-types.test.js +203 -0
  4. package/dist/__tests__/var-allocator.test.d.ts +1 -0
  5. package/dist/__tests__/var-allocator.test.js +69 -0
  6. package/dist/ast/types.d.ts +2 -1
  7. package/dist/cli.js +17 -6
  8. package/dist/codegen/mcfunction/index.d.ts +2 -0
  9. package/dist/codegen/mcfunction/index.js +52 -43
  10. package/dist/codegen/structure/index.d.ts +4 -1
  11. package/dist/codegen/structure/index.js +8 -12
  12. package/dist/codegen/var-allocator.d.ts +28 -0
  13. package/dist/codegen/var-allocator.js +78 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +8 -6
  16. package/dist/lowering/index.d.ts +2 -0
  17. package/dist/lowering/index.js +62 -3
  18. package/dist/parser/index.js +22 -1
  19. package/dist/typechecker/index.js +30 -0
  20. package/dist/types/entity-hierarchy.d.ts +29 -0
  21. package/dist/types/entity-hierarchy.js +107 -0
  22. package/editors/vscode/package-lock.json +6 -4
  23. package/editors/vscode/package.json +3 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/entity-types.test.ts +236 -0
  26. package/src/__tests__/var-allocator.test.ts +75 -0
  27. package/src/ast/types.ts +8 -4
  28. package/src/cli.ts +20 -6
  29. package/src/codegen/mcfunction/index.ts +60 -48
  30. package/src/codegen/structure/index.ts +9 -14
  31. package/src/codegen/var-allocator.ts +75 -0
  32. package/src/examples/capture_the_flag.mcrs +34 -34
  33. package/src/examples/hunger_games.mcrs +59 -59
  34. package/src/examples/new_features_demo.mcrs +32 -32
  35. package/src/examples/parkour_race.mcrs +58 -58
  36. package/src/index.ts +10 -6
  37. package/src/lowering/index.ts +73 -8
  38. package/src/parser/index.ts +20 -1
  39. package/src/typechecker/index.ts +30 -0
  40. package/src/types/entity-hierarchy.ts +120 -0
  41. package/dist/data/arena/function/__load.mcfunction +0 -6
  42. package/dist/data/arena/function/__tick.mcfunction +0 -2
  43. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +0 -3
  44. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +0 -1
  45. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +0 -3
  46. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +0 -7
  47. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +0 -1
  48. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +0 -4
  49. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +0 -6
  50. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +0 -1
  51. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +0 -4
  52. package/dist/data/arena/function/announce_leaders.mcfunction +0 -6
  53. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +0 -1
  54. package/dist/data/arena/function/arena_tick/then_0.mcfunction +0 -4
  55. package/dist/data/arena/function/arena_tick.mcfunction +0 -11
  56. package/dist/data/counter/function/__load.mcfunction +0 -5
  57. package/dist/data/counter/function/__tick.mcfunction +0 -2
  58. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +0 -1
  59. package/dist/data/counter/function/counter_tick/then_0.mcfunction +0 -3
  60. package/dist/data/counter/function/counter_tick.mcfunction +0 -11
  61. package/dist/data/minecraft/tags/function/load.json +0 -5
  62. package/dist/data/minecraft/tags/function/tick.json +0 -5
  63. package/dist/data/quiz/function/__load.mcfunction +0 -16
  64. package/dist/data/quiz/function/__tick.mcfunction +0 -6
  65. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +0 -4
  66. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +0 -4
  67. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +0 -4
  68. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +0 -4
  69. package/dist/data/quiz/function/answer_a.mcfunction +0 -4
  70. package/dist/data/quiz/function/answer_b.mcfunction +0 -4
  71. package/dist/data/quiz/function/answer_c.mcfunction +0 -4
  72. package/dist/data/quiz/function/ask_question/else_1.mcfunction +0 -5
  73. package/dist/data/quiz/function/ask_question/else_4.mcfunction +0 -5
  74. package/dist/data/quiz/function/ask_question/else_7.mcfunction +0 -4
  75. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +0 -1
  76. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +0 -2
  77. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +0 -2
  78. package/dist/data/quiz/function/ask_question/then_0.mcfunction +0 -4
  79. package/dist/data/quiz/function/ask_question/then_3.mcfunction +0 -4
  80. package/dist/data/quiz/function/ask_question/then_6.mcfunction +0 -4
  81. package/dist/data/quiz/function/ask_question.mcfunction +0 -7
  82. package/dist/data/quiz/function/finish_quiz.mcfunction +0 -6
  83. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +0 -5
  84. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +0 -3
  85. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +0 -3
  86. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +0 -3
  87. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +0 -5
  88. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +0 -2
  89. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +0 -2
  90. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +0 -2
  91. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +0 -8
  92. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +0 -2
  93. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +0 -2
  94. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +0 -5
  95. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +0 -5
  96. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +0 -6
  97. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +0 -6
  98. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +0 -5
  99. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +0 -6
  100. package/dist/data/quiz/function/handle_answer.mcfunction +0 -11
  101. package/dist/data/quiz/function/start_quiz.mcfunction +0 -5
  102. package/dist/data/shop/function/__load.mcfunction +0 -7
  103. package/dist/data/shop/function/__tick.mcfunction +0 -3
  104. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +0 -4
  105. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +0 -5
  106. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +0 -5
  107. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +0 -3
  108. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +0 -2
  109. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +0 -2
  110. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +0 -2
  111. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +0 -4
  112. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +0 -4
  113. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +0 -4
  114. package/dist/data/shop/function/complete_purchase.mcfunction +0 -7
  115. package/dist/data/shop/function/handle_shop_trigger.mcfunction +0 -3
  116. package/dist/data/turret/function/__load.mcfunction +0 -5
  117. package/dist/data/turret/function/__tick.mcfunction +0 -4
  118. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +0 -4
  119. package/dist/data/turret/function/deploy_turret.mcfunction +0 -8
  120. package/dist/data/turret/function/turret_tick/at_1.mcfunction +0 -2
  121. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +0 -2
  122. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +0 -2
  123. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +0 -3
  124. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +0 -1
  125. package/dist/data/turret/function/turret_tick.mcfunction +0 -5
  126. package/dist/pack.mcmeta +0 -6
package/src/cli.ts CHANGED
@@ -53,6 +53,7 @@ Options:
53
53
  --namespace <ns> Datapack namespace (default: derived from filename)
54
54
  --target <target> Output target: datapack (default), cmdblock, or structure
55
55
  --no-dce Disable AST dead code elimination
56
+ --no-mangle Disable variable name mangling (use readable names)
56
57
  --stats Print optimizer statistics
57
58
  --hot-reload <url> After each successful compile, POST to <url>/reload
58
59
  (use with redscript-testharness; e.g. http://localhost:25561)
@@ -166,8 +167,9 @@ function parseArgs(args: string[]): {
166
167
  help?: boolean
167
168
  hotReload?: string
168
169
  dce?: boolean
170
+ mangle?: boolean
169
171
  } {
170
- const result: ReturnType<typeof parseArgs> = { dce: true }
172
+ const result: ReturnType<typeof parseArgs> = { dce: true, mangle: true }
171
173
  let i = 0
172
174
 
173
175
  while (i < args.length) {
@@ -194,6 +196,9 @@ function parseArgs(args: string[]): {
194
196
  } else if (arg === '--no-dce') {
195
197
  result.dce = false
196
198
  i++
199
+ } else if (arg === '--no-mangle') {
200
+ result.mangle = false
201
+ i++
197
202
  } else if (arg === '--hot-reload') {
198
203
  result.hotReload = args[++i]
199
204
  i++
@@ -256,7 +261,8 @@ function compileCommand(
256
261
  namespace: string,
257
262
  target: string = 'datapack',
258
263
  showStats = false,
259
- dce = true
264
+ dce = true,
265
+ mangle = true
260
266
  ): void {
261
267
  // Read source file
262
268
  if (!fs.existsSync(file)) {
@@ -268,7 +274,7 @@ function compileCommand(
268
274
 
269
275
  try {
270
276
  if (target === 'cmdblock') {
271
- const result = compile(source, { namespace, filePath: file, dce })
277
+ const result = compile(source, { namespace, filePath: file, dce, mangle })
272
278
  printWarnings(result.warnings)
273
279
 
274
280
  // Generate command block JSON
@@ -288,7 +294,7 @@ function compileCommand(
288
294
  printOptimizationStats(result.stats)
289
295
  }
290
296
  } else if (target === 'structure') {
291
- const structure = compileToStructure(source, namespace, file, { dce })
297
+ const structure = compileToStructure(source, namespace, file, { dce, mangle })
292
298
  fs.mkdirSync(path.dirname(output), { recursive: true })
293
299
  fs.writeFileSync(output, structure.buffer)
294
300
 
@@ -299,7 +305,7 @@ function compileCommand(
299
305
  printOptimizationStats(structure.stats)
300
306
  }
301
307
  } else {
302
- const result = compile(source, { namespace, filePath: file, dce })
308
+ const result = compile(source, { namespace, filePath: file, dce, mangle })
303
309
  printWarnings(result.warnings)
304
310
 
305
311
  // Default: generate datapack
@@ -314,6 +320,13 @@ function compileCommand(
314
320
  fs.writeFileSync(filePath, dataFile.content)
315
321
  }
316
322
 
323
+ // Write sourcemap alongside datapack when mangle mode is active
324
+ if (mangle && result.sourceMap && Object.keys(result.sourceMap).length > 0) {
325
+ const mapPath = path.join(output, `${namespace}.map.json`)
326
+ fs.writeFileSync(mapPath, JSON.stringify(result.sourceMap, null, 2))
327
+ console.log(` Sourcemap: ${mapPath}`)
328
+ }
329
+
317
330
  console.log(`✓ Compiled ${file} to ${output}/`)
318
331
  console.log(` Namespace: ${namespace}`)
319
332
  console.log(` Functions: ${result.ir.functions.length}`)
@@ -494,7 +507,8 @@ async function main(): Promise<void> {
494
507
  namespace,
495
508
  target,
496
509
  parsed.stats,
497
- parsed.dce
510
+ parsed.dce,
511
+ parsed.mangle
498
512
  )
499
513
  }
500
514
  break
@@ -19,6 +19,7 @@
19
19
  import type { IRBlock, IRFunction, IRModule, Operand, Terminator } from '../../ir/types'
20
20
  import { optimizeCommandFunctions, type OptimizationStats, createEmptyOptimizationStats, mergeOptimizationStats } from '../../optimizer/commands'
21
21
  import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
22
+ import { VarAllocator } from '../var-allocator'
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Utilities
@@ -26,21 +27,12 @@ import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/t
26
27
 
27
28
  const OBJ = 'rs' // scoreboard objective name
28
29
 
29
- function varRef(name: string): string {
30
- // Ensure fake player prefix
31
- return name.startsWith('$') ? name : `$${name}`
32
- }
33
-
34
- function operandToScore(op: Operand): string {
35
- if (op.kind === 'var') return `${varRef(op.name)} ${OBJ}`
36
- if (op.kind === 'const') return `$const_${op.value} ${OBJ}`
30
+ function operandToScore(op: Operand, alloc: VarAllocator): string {
31
+ if (op.kind === 'var') return `${alloc.alloc(op.name)} ${OBJ}`
32
+ if (op.kind === 'const') return `${alloc.constant(op.value)} ${OBJ}`
37
33
  throw new Error(`Cannot convert storage operand to score: ${op.path}`)
38
34
  }
39
35
 
40
- function constSetup(value: number): string {
41
- return `scoreboard players set $const_${value} ${OBJ} ${value}`
42
- }
43
-
44
36
  // Collect all constants used in a function for pre-setup
45
37
  function collectConsts(fn: IRFunction): Set<number> {
46
38
  const consts = new Set<number>()
@@ -71,17 +63,17 @@ const BOP_OP: Record<string, string> = {
71
63
  // Instruction codegen
72
64
  // ---------------------------------------------------------------------------
73
65
 
74
- function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns: string): string[] {
66
+ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns: string, alloc: VarAllocator): string[] {
75
67
  const lines: string[] = []
76
68
 
77
69
  switch (instr.op) {
78
70
  case 'assign': {
79
- const dst = varRef(instr.dst)
71
+ const dst = alloc.alloc(instr.dst)
80
72
  const src = instr.src as Operand
81
73
  if (src.kind === 'const') {
82
74
  lines.push(`scoreboard players set ${dst} ${OBJ} ${src.value}`)
83
75
  } else if (src.kind === 'var') {
84
- lines.push(`scoreboard players operation ${dst} ${OBJ} = ${varRef(src.name)} ${OBJ}`)
76
+ lines.push(`scoreboard players operation ${dst} ${OBJ} = ${alloc.alloc(src.name)} ${OBJ}`)
85
77
  } else {
86
78
  lines.push(`execute store result score ${dst} ${OBJ} run data get storage ${src.path}`)
87
79
  }
@@ -89,19 +81,19 @@ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns:
89
81
  }
90
82
 
91
83
  case 'binop': {
92
- const dst = varRef(instr.dst)
84
+ const dst = alloc.alloc(instr.dst)
93
85
  const bop = BOP_OP[instr.bop as string] ?? '+='
94
86
  // Copy lhs → dst, then apply op with rhs
95
- lines.push(...emitInstr({ op: 'assign', dst: instr.dst, src: instr.lhs }, ns))
96
- lines.push(`scoreboard players operation ${dst} ${OBJ} ${bop} ${operandToScore(instr.rhs)}`)
87
+ lines.push(...emitInstr({ op: 'assign', dst: instr.dst, src: instr.lhs }, ns, alloc))
88
+ lines.push(`scoreboard players operation ${dst} ${OBJ} ${bop} ${operandToScore(instr.rhs, alloc)}`)
97
89
  break
98
90
  }
99
91
 
100
92
  case 'cmp': {
101
93
  // MC doesn't have a direct compare-to-register; use execute store
102
- const dst = varRef(instr.dst)
103
- const lhsScore = operandToScore(instr.lhs)
104
- const rhsScore = operandToScore(instr.rhs)
94
+ const dst = alloc.alloc(instr.dst)
95
+ const lhsScore = operandToScore(instr.lhs, alloc)
96
+ const rhsScore = operandToScore(instr.rhs, alloc)
105
97
  lines.push(`scoreboard players set ${dst} ${OBJ} 0`)
106
98
  switch (instr.cop) {
107
99
  case '==':
@@ -127,13 +119,15 @@ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns:
127
119
  }
128
120
 
129
121
  case 'call': {
130
- // Push args as fake players $p0, $p1, ...
122
+ // Push args as param fake players
131
123
  for (let i = 0; i < instr.args.length; i++) {
132
- lines.push(...emitInstr({ op: 'assign', dst: `$p${i}`, src: instr.args[i] }, ns))
124
+ const paramName = alloc.internal(`p${i}`)
125
+ lines.push(...emitInstr({ op: 'assign', dst: paramName, src: instr.args[i] }, ns, alloc))
133
126
  }
134
127
  lines.push(`function ${ns}:${instr.fn}`)
135
128
  if (instr.dst) {
136
- lines.push(`scoreboard players operation ${varRef(instr.dst)} ${OBJ} = $ret ${OBJ}`)
129
+ const retName = alloc.internal('ret')
130
+ lines.push(`scoreboard players operation ${alloc.alloc(instr.dst)} ${OBJ} = ${retName} ${OBJ}`)
137
131
  }
138
132
  break
139
133
  }
@@ -150,31 +144,33 @@ function emitInstr(instr: ReturnType<typeof Object.assign> & { op: string }, ns:
150
144
  // Terminator codegen
151
145
  // ---------------------------------------------------------------------------
152
146
 
153
- function emitTerm(term: Terminator, ns: string, fnName: string): string[] {
147
+ function emitTerm(term: Terminator, ns: string, fnName: string, alloc: VarAllocator): string[] {
154
148
  const lines: string[] = []
155
149
  switch (term.op) {
156
150
  case 'jump':
157
151
  lines.push(`function ${ns}:${fnName}/${term.target}`)
158
152
  break
159
153
  case 'jump_if':
160
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.then}`)
161
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.else_}`)
154
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.then}`)
155
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.else_}`)
162
156
  break
163
157
  case 'jump_unless':
164
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.then}`)
165
- lines.push(`execute if score ${varRef(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.else_}`)
158
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches ..0 run function ${ns}:${fnName}/${term.then}`)
159
+ lines.push(`execute if score ${alloc.alloc(term.cond)} ${OBJ} matches 1.. run function ${ns}:${fnName}/${term.else_}`)
166
160
  break
167
- case 'return':
161
+ case 'return': {
162
+ const retName = alloc.internal('ret')
168
163
  if (term.value) {
169
- lines.push(...emitInstr({ op: 'assign', dst: '$ret', src: term.value }, ns))
164
+ lines.push(...emitInstr({ op: 'assign', dst: retName, src: term.value }, ns, alloc))
170
165
  }
171
166
  // In MC 1.20+, use `return` command
172
167
  if (term.value?.kind === 'const') {
173
168
  lines.push(`return ${term.value.value}`)
174
169
  } else if (term.value?.kind === 'var') {
175
- lines.push(`return run scoreboard players get ${varRef(term.value.name)} ${OBJ}`)
170
+ lines.push(`return run scoreboard players get ${alloc.alloc(term.value.name)} ${OBJ}`)
176
171
  }
177
172
  break
173
+ }
178
174
  case 'tick_yield':
179
175
  lines.push(`schedule function ${ns}:${fnName}/${term.continuation} 1t replace`)
180
176
  break
@@ -220,7 +216,7 @@ function applyFunctionOptimization(
220
216
 
221
217
  // Filter out files for functions that were removed (inlined trivial functions)
222
218
  const optimizedNames = new Set(optimized.functions.map(fn => fn.name))
223
-
219
+
224
220
  return {
225
221
  files: files
226
222
  .filter(file => {
@@ -248,10 +244,12 @@ export interface DatapackGenerationResult {
248
244
  files: DatapackFile[]
249
245
  advancements: DatapackFile[]
250
246
  stats: OptimizationStats
247
+ sourceMap?: Record<string, string>
251
248
  }
252
249
 
253
250
  export interface DatapackGenerationOptions {
254
251
  optimizeCommands?: boolean
252
+ mangle?: boolean
255
253
  }
256
254
 
257
255
  export function countMcfunctionCommands(files: DatapackFile[]): number {
@@ -272,7 +270,8 @@ export function generateDatapackWithStats(
272
270
  module: IRModule,
273
271
  options: DatapackGenerationOptions = {},
274
272
  ): DatapackGenerationResult {
275
- const { optimizeCommands = true } = options
273
+ const { optimizeCommands = true, mangle = false } = options
274
+ const alloc = new VarAllocator(mangle)
276
275
  const files: DatapackFile[] = []
277
276
  const advancements: DatapackFile[] = []
278
277
  const ns = module.namespace
@@ -307,7 +306,7 @@ export function generateDatapackWithStats(
307
306
  `scoreboard objectives add ${OBJ} dummy`,
308
307
  ]
309
308
  for (const g of module.globals) {
310
- loadLines.push(`scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`)
309
+ loadLines.push(`scoreboard players set ${alloc.alloc(g.name)} ${OBJ} ${g.init}`)
311
310
  }
312
311
 
313
312
  // Add trigger objectives
@@ -349,13 +348,19 @@ export function generateDatapackWithStats(
349
348
  })
350
349
  }
351
350
 
352
- // Generate each function (and collect constants for load)
351
+ // Collect all constants across all functions first (deduplicated)
352
+ const allConsts = new Set<number>()
353
+ for (const fn of module.functions) {
354
+ for (const c of collectConsts(fn)) allConsts.add(c)
355
+ }
356
+ if (allConsts.size > 0) {
357
+ loadLines.push(...Array.from(allConsts).sort((a, b) => a - b).map(
358
+ value => `scoreboard players set ${alloc.constant(value)} ${OBJ} ${value}`
359
+ ))
360
+ }
361
+
362
+ // Generate each function
353
363
  for (const fn of module.functions) {
354
- // Constant setup — place constants in __load.mcfunction
355
- const consts = collectConsts(fn)
356
- if (consts.size > 0) {
357
- loadLines.push(...Array.from(consts).map(constSetup))
358
- }
359
364
 
360
365
  // Entry block → <fn_name>.mcfunction
361
366
  // Continuation blocks → <fn_name>/<label>.mcfunction
@@ -366,19 +371,24 @@ export function generateDatapackWithStats(
366
371
  // Param setup in entry block
367
372
  if (i === 0) {
368
373
  for (let j = 0; j < fn.params.length; j++) {
369
- lines.push(`scoreboard players operation ${varRef(fn.params[j])} ${OBJ} = $p${j} ${OBJ}`)
374
+ lines.push(`scoreboard players operation ${alloc.alloc(fn.params[j])} ${OBJ} = ${alloc.internal(`p${j}`)} ${OBJ}`)
370
375
  }
371
376
  }
372
377
 
373
378
  for (const instr of block.instrs) {
374
- lines.push(...emitInstr(instr as any, ns))
379
+ lines.push(...emitInstr(instr as any, ns, alloc))
375
380
  }
376
- lines.push(...emitTerm(block.term, ns, fn.name))
381
+ lines.push(...emitTerm(block.term, ns, fn.name, alloc))
377
382
 
378
383
  const filePath = i === 0
379
384
  ? `data/${ns}/function/${fn.name}.mcfunction`
380
385
  : `data/${ns}/function/${fn.name}/${block.label}.mcfunction`
381
386
 
387
+ // Skip empty continuation blocks (only contain the block comment, no real commands)
388
+ // Entry block (i === 0) is always emitted so the function file exists
389
+ const hasRealContent = lines.some(l => !l.startsWith('#') && l.trim() !== '')
390
+ if (i !== 0 && !hasRealContent) continue
391
+
382
392
  files.push({ path: filePath, content: lines.join('\n') })
383
393
  }
384
394
  }
@@ -404,12 +414,12 @@ export function generateDatapackWithStats(
404
414
 
405
415
  // __tick.mcfunction — calls all @tick functions + trigger check
406
416
  const tickLines = ['# RedScript tick dispatcher']
407
-
417
+
408
418
  // Call all @tick functions
409
419
  for (const fnName of tickFunctionNames) {
410
420
  tickLines.push(`function ${ns}:${fnName}`)
411
421
  }
412
-
422
+
413
423
  // Call trigger check if there are triggers
414
424
  if (triggerNames.size > 0) {
415
425
  tickLines.push(`# Trigger checks`)
@@ -502,13 +512,15 @@ export function generateDatapackWithStats(
502
512
  }
503
513
 
504
514
  const stats = createEmptyOptimizationStats()
515
+ const sourceMap = mangle ? alloc.toSourceMap() : undefined
516
+
505
517
  if (!optimizeCommands) {
506
- return { files, advancements, stats }
518
+ return { files, advancements, stats, sourceMap }
507
519
  }
508
520
 
509
521
  const optimized = applyFunctionOptimization(files)
510
522
  mergeOptimizationStats(stats, optimized.stats)
511
- return { files: optimized.files, advancements, stats }
523
+ return { files: optimized.files, advancements, stats, sourceMap }
512
524
  }
513
525
 
514
526
  export function generateDatapack(module: IRModule): DatapackFile[] {
@@ -10,6 +10,7 @@ import { preprocessSource } from '../../compile'
10
10
  import type { IRCommand, IRFunction, IRModule } from '../../ir/types'
11
11
  import type { DatapackFile } from '../mcfunction'
12
12
  import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
13
+ import { VarAllocator } from '../var-allocator'
13
14
 
14
15
  const DATA_VERSION = 3953
15
16
  const MAX_WIDTH = 16
@@ -54,16 +55,13 @@ export interface StructureCompileResult {
54
55
 
55
56
  export interface StructureCompileOptions {
56
57
  dce?: boolean
58
+ mangle?: boolean
57
59
  }
58
60
 
59
61
  function escapeJsonString(value: string): string {
60
62
  return JSON.stringify(value).slice(1, -1)
61
63
  }
62
64
 
63
- function varRef(name: string): string {
64
- return name.startsWith('$') ? name : `$${name}`
65
- }
66
-
67
65
  function collectConsts(fn: IRFunction): Set<number> {
68
66
  const consts = new Set<number>()
69
67
  for (const block of fn.blocks) {
@@ -85,11 +83,8 @@ function collectConsts(fn: IRFunction): Set<number> {
85
83
  return consts
86
84
  }
87
85
 
88
- function constSetup(value: number): string {
89
- return `scoreboard players set $const_${value} ${OBJ} ${value}`
90
- }
91
-
92
- function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
86
+ function collectCommandEntriesFromModule(module: IRModule, mangle = false): CommandEntry[] {
87
+ const alloc = new VarAllocator(mangle)
93
88
  const entries: CommandEntry[] = []
94
89
  const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
95
90
  const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
@@ -99,14 +94,14 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
99
94
  const eventTypes = new Set<EventTypeName>(eventHandlers.map(fn => fn.eventHandler.eventType))
100
95
  const loadCommands = [
101
96
  `scoreboard objectives add ${OBJ} dummy`,
102
- ...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
97
+ ...module.globals.map(g => `scoreboard players set ${alloc.alloc(g.name)} ${OBJ} ${g.init}`),
103
98
  ...Array.from(triggerNames).flatMap(triggerName => [
104
99
  `scoreboard objectives add ${triggerName} trigger`,
105
100
  `scoreboard players enable @a ${triggerName}`,
106
101
  ]),
107
102
  ...Array.from(
108
103
  new Set(module.functions.flatMap(fn => Array.from(collectConsts(fn))))
109
- ).map(constSetup),
104
+ ).map(value => `scoreboard players set ${alloc.constant(value)} ${OBJ} ${value}`),
110
105
  ]
111
106
 
112
107
  for (const eventType of eventTypes) {
@@ -289,10 +284,10 @@ function createBlockTag(entry: CommandEntry, index: number): CompoundTag {
289
284
  })
290
285
  }
291
286
 
292
- export function generateStructure(input: IRModule | DatapackFile[]): StructureCompileResult {
287
+ export function generateStructure(input: IRModule | DatapackFile[], options?: { mangle?: boolean }): StructureCompileResult {
293
288
  const entries = Array.isArray(input)
294
289
  ? collectCommandEntriesFromFiles(input)
295
- : collectCommandEntriesFromModule(input)
290
+ : collectCommandEntriesFromModule(input, options?.mangle)
296
291
 
297
292
  const blockTags = entries.map(createBlockTag)
298
293
  const sizeX = Math.max(1, Math.min(MAX_WIDTH, entries.length || 1))
@@ -345,7 +340,7 @@ export function compileToStructure(
345
340
  functions: structureOptimized.functions,
346
341
  }
347
342
  return {
348
- ...generateStructure(optimizedModule),
343
+ ...generateStructure(optimizedModule, { mangle: options.mangle }),
349
344
  stats,
350
345
  }
351
346
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * VarAllocator — assigns scoreboard fake-player names to variables.
3
+ *
4
+ * mangle=true: sequential short names ($a, $b, ..., $z, $aa, $ab, ...)
5
+ * mangle=false: legacy names ($<name> for vars, $const_<v> for consts, $p0/$ret for internals)
6
+ */
7
+
8
+ export class VarAllocator {
9
+ private readonly mangle: boolean
10
+ private seq = 0
11
+ private readonly varCache = new Map<string, string>()
12
+ private readonly constCache = new Map<number, string>()
13
+ private readonly internalCache = new Map<string, string>()
14
+
15
+ constructor(mangle = true) {
16
+ this.mangle = mangle
17
+ }
18
+
19
+ /** Allocate a name for a user variable. Strips leading '$' if present. */
20
+ alloc(originalName: string): string {
21
+ const clean = originalName.startsWith('$') ? originalName.slice(1) : originalName
22
+ const cached = this.varCache.get(clean)
23
+ if (cached) return cached
24
+ const name = this.mangle ? `$${this.nextSeqName()}` : `$${clean}`
25
+ this.varCache.set(clean, name)
26
+ return name
27
+ }
28
+
29
+ /** Allocate a name for a constant value (content-addressed). */
30
+ constant(value: number): string {
31
+ const cached = this.constCache.get(value)
32
+ if (cached) return cached
33
+ const name = this.mangle ? `$${this.nextSeqName()}` : `$const_${value}`
34
+ this.constCache.set(value, name)
35
+ return name
36
+ }
37
+
38
+ /** Allocate a name for a compiler internal (e.g. "ret", "p0"). */
39
+ internal(suffix: string): string {
40
+ const cached = this.internalCache.get(suffix)
41
+ if (cached) return cached
42
+ const name = this.mangle ? `$${this.nextSeqName()}` : `$${suffix}`
43
+ this.internalCache.set(suffix, name)
44
+ return name
45
+ }
46
+
47
+ /** Generate the next sequential name: a, b, ..., z, aa, ab, ..., az, ba, ... */
48
+ private nextSeqName(): string {
49
+ const n = this.seq++
50
+ let result = ''
51
+ let remaining = n
52
+ do {
53
+ result = String.fromCharCode(97 + (remaining % 26)) + result
54
+ remaining = Math.floor(remaining / 26) - 1
55
+ } while (remaining >= 0)
56
+ return result
57
+ }
58
+
59
+ /**
60
+ * Returns a sourcemap object mapping allocated name → original name.
61
+ * Useful for debugging: write to <output>.map.json alongside the datapack.
62
+ * Only meaningful when mangle=true.
63
+ */
64
+ toSourceMap(): Record<string, string> {
65
+ const map: Record<string, string> = {}
66
+ for (const [orig, alloc] of this.varCache) {
67
+ // Skip compiler-generated temporaries (start with _ followed by digits)
68
+ if (/^_\d+$/.test(orig)) continue
69
+ map[alloc] = orig
70
+ }
71
+ for (const [val, alloc] of this.constCache) map[alloc] = `const:${val}`
72
+ for (const [suf, alloc] of this.internalCache) map[alloc] = `internal:${suf}`
73
+ return map
74
+ }
75
+ }