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.
- package/.github/workflows/publish-extension-on-ci.yml +100 -0
- package/dist/__tests__/entity-types.test.d.ts +1 -0
- package/dist/__tests__/entity-types.test.js +203 -0
- package/dist/__tests__/var-allocator.test.d.ts +1 -0
- package/dist/__tests__/var-allocator.test.js +69 -0
- package/dist/ast/types.d.ts +2 -1
- package/dist/cli.js +17 -6
- package/dist/codegen/mcfunction/index.d.ts +2 -0
- package/dist/codegen/mcfunction/index.js +52 -43
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +8 -12
- package/dist/codegen/var-allocator.d.ts +28 -0
- package/dist/codegen/var-allocator.js +78 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -6
- package/dist/lowering/index.d.ts +2 -0
- package/dist/lowering/index.js +62 -3
- package/dist/parser/index.js +22 -1
- package/dist/typechecker/index.js +30 -0
- package/dist/types/entity-hierarchy.d.ts +29 -0
- package/dist/types/entity-hierarchy.js +107 -0
- package/editors/vscode/package-lock.json +6 -4
- package/editors/vscode/package.json +3 -3
- package/package.json +1 -1
- package/src/__tests__/entity-types.test.ts +236 -0
- package/src/__tests__/var-allocator.test.ts +75 -0
- package/src/ast/types.ts +8 -4
- package/src/cli.ts +20 -6
- package/src/codegen/mcfunction/index.ts +60 -48
- package/src/codegen/structure/index.ts +9 -14
- package/src/codegen/var-allocator.ts +75 -0
- package/src/examples/capture_the_flag.mcrs +34 -34
- package/src/examples/hunger_games.mcrs +59 -59
- package/src/examples/new_features_demo.mcrs +32 -32
- package/src/examples/parkour_race.mcrs +58 -58
- package/src/index.ts +10 -6
- package/src/lowering/index.ts +73 -8
- package/src/parser/index.ts +20 -1
- package/src/typechecker/index.ts +30 -0
- package/src/types/entity-hierarchy.ts +120 -0
- package/dist/data/arena/function/__load.mcfunction +0 -6
- package/dist/data/arena/function/__tick.mcfunction +0 -2
- package/dist/data/arena/function/announce_leaders/else_1.mcfunction +0 -3
- package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +0 -1
- package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +0 -3
- package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +0 -7
- package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +0 -1
- package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +0 -4
- package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +0 -6
- package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +0 -1
- package/dist/data/arena/function/announce_leaders/then_0.mcfunction +0 -4
- package/dist/data/arena/function/announce_leaders.mcfunction +0 -6
- package/dist/data/arena/function/arena_tick/merge_2.mcfunction +0 -1
- package/dist/data/arena/function/arena_tick/then_0.mcfunction +0 -4
- package/dist/data/arena/function/arena_tick.mcfunction +0 -11
- package/dist/data/counter/function/__load.mcfunction +0 -5
- package/dist/data/counter/function/__tick.mcfunction +0 -2
- package/dist/data/counter/function/counter_tick/merge_2.mcfunction +0 -1
- package/dist/data/counter/function/counter_tick/then_0.mcfunction +0 -3
- package/dist/data/counter/function/counter_tick.mcfunction +0 -11
- package/dist/data/minecraft/tags/function/load.json +0 -5
- package/dist/data/minecraft/tags/function/tick.json +0 -5
- package/dist/data/quiz/function/__load.mcfunction +0 -16
- package/dist/data/quiz/function/__tick.mcfunction +0 -6
- package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +0 -4
- package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +0 -4
- package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +0 -4
- package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +0 -4
- package/dist/data/quiz/function/answer_a.mcfunction +0 -4
- package/dist/data/quiz/function/answer_b.mcfunction +0 -4
- package/dist/data/quiz/function/answer_c.mcfunction +0 -4
- package/dist/data/quiz/function/ask_question/else_1.mcfunction +0 -5
- package/dist/data/quiz/function/ask_question/else_4.mcfunction +0 -5
- package/dist/data/quiz/function/ask_question/else_7.mcfunction +0 -4
- package/dist/data/quiz/function/ask_question/merge_2.mcfunction +0 -1
- package/dist/data/quiz/function/ask_question/merge_5.mcfunction +0 -2
- package/dist/data/quiz/function/ask_question/merge_8.mcfunction +0 -2
- package/dist/data/quiz/function/ask_question/then_0.mcfunction +0 -4
- package/dist/data/quiz/function/ask_question/then_3.mcfunction +0 -4
- package/dist/data/quiz/function/ask_question/then_6.mcfunction +0 -4
- package/dist/data/quiz/function/ask_question.mcfunction +0 -7
- package/dist/data/quiz/function/finish_quiz.mcfunction +0 -6
- package/dist/data/quiz/function/handle_answer/else_1.mcfunction +0 -5
- package/dist/data/quiz/function/handle_answer/else_10.mcfunction +0 -3
- package/dist/data/quiz/function/handle_answer/else_16.mcfunction +0 -3
- package/dist/data/quiz/function/handle_answer/else_4.mcfunction +0 -3
- package/dist/data/quiz/function/handle_answer/else_7.mcfunction +0 -5
- package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +0 -2
- package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +0 -2
- package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +0 -2
- package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +0 -8
- package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +0 -2
- package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +0 -2
- package/dist/data/quiz/function/handle_answer/then_0.mcfunction +0 -5
- package/dist/data/quiz/function/handle_answer/then_12.mcfunction +0 -5
- package/dist/data/quiz/function/handle_answer/then_15.mcfunction +0 -6
- package/dist/data/quiz/function/handle_answer/then_3.mcfunction +0 -6
- package/dist/data/quiz/function/handle_answer/then_6.mcfunction +0 -5
- package/dist/data/quiz/function/handle_answer/then_9.mcfunction +0 -6
- package/dist/data/quiz/function/handle_answer.mcfunction +0 -11
- package/dist/data/quiz/function/start_quiz.mcfunction +0 -5
- package/dist/data/shop/function/__load.mcfunction +0 -7
- package/dist/data/shop/function/__tick.mcfunction +0 -3
- package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +0 -4
- package/dist/data/shop/function/complete_purchase/else_1.mcfunction +0 -5
- package/dist/data/shop/function/complete_purchase/else_4.mcfunction +0 -5
- package/dist/data/shop/function/complete_purchase/else_7.mcfunction +0 -3
- package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +0 -2
- package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +0 -2
- package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +0 -2
- package/dist/data/shop/function/complete_purchase/then_0.mcfunction +0 -4
- package/dist/data/shop/function/complete_purchase/then_3.mcfunction +0 -4
- package/dist/data/shop/function/complete_purchase/then_6.mcfunction +0 -4
- package/dist/data/shop/function/complete_purchase.mcfunction +0 -7
- package/dist/data/shop/function/handle_shop_trigger.mcfunction +0 -3
- package/dist/data/turret/function/__load.mcfunction +0 -5
- package/dist/data/turret/function/__tick.mcfunction +0 -4
- package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +0 -4
- package/dist/data/turret/function/deploy_turret.mcfunction +0 -8
- package/dist/data/turret/function/turret_tick/at_1.mcfunction +0 -2
- package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +0 -2
- package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +0 -2
- package/dist/data/turret/function/turret_tick/tick_body.mcfunction +0 -3
- package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +0 -1
- package/dist/data/turret/function/turret_tick.mcfunction +0 -5
- 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
|
|
30
|
-
|
|
31
|
-
|
|
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 =
|
|
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} = ${
|
|
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 =
|
|
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 =
|
|
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
|
|
122
|
+
// Push args as param fake players
|
|
131
123
|
for (let i = 0; i < instr.args.length; i++) {
|
|
132
|
-
|
|
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
|
-
|
|
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 ${
|
|
161
|
-
lines.push(`execute if score ${
|
|
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 ${
|
|
165
|
-
lines.push(`execute if score ${
|
|
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:
|
|
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 ${
|
|
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 ${
|
|
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
|
-
//
|
|
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 ${
|
|
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
|
|
89
|
-
|
|
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 ${
|
|
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(
|
|
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
|
+
}
|