redscript-mc 1.2.27 → 1.2.28

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.
@@ -1,12 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setOptimizerObjective = setOptimizerObjective;
3
4
  exports.createEmptyOptimizationStats = createEmptyOptimizationStats;
4
5
  exports.mergeOptimizationStats = mergeOptimizationStats;
5
6
  exports.applyLICM = applyLICM;
6
7
  exports.applyCSE = applyCSE;
7
8
  exports.batchSetblocks = batchSetblocks;
8
9
  exports.optimizeCommandFunctions = optimizeCommandFunctions;
9
- const SCOREBOARD_READ_RE = /^execute store result score (\$[A-Za-z0-9_]+) rs run scoreboard players get (\S+) (\S+)$/;
10
+ // Matches scoreboard reads for LICM/CSE objective is captured in group 2
11
+ // so the optimizer can reconstruct the command with the same objective.
12
+ let _OBJ_PATTERN = 'rs';
13
+ function setOptimizerObjective(obj) { _OBJ_PATTERN = obj; }
14
+ function scoreboardReadRe() {
15
+ return new RegExp(`^execute store result score (\\$[A-Za-z0-9_]+) ${_OBJ_PATTERN} run scoreboard players get (\\S+) (\\S+)$`);
16
+ }
10
17
  const SCOREBOARD_WRITE_RE = /^(?:scoreboard players (?:set|add|remove|reset)\s+(\S+)\s+(\S+)|scoreboard players operation\s+(\S+)\s+(\S+)\s+[+\-*/%]?= )/;
11
18
  const EXECUTE_STORE_SCORE_RE = /^execute store result score (\S+) (\S+) run /;
12
19
  const FUNCTION_CALL_RE = /^execute as (.+) run function ([^:]+):(.+)$/;
@@ -95,7 +102,7 @@ function applyLICMInternal(functions) {
95
102
  const readInfo = new Map();
96
103
  const scoreboardWrites = new Set();
97
104
  for (const inner of loopFn.commands) {
98
- const readMatch = inner.cmd.match(SCOREBOARD_READ_RE);
105
+ const readMatch = inner.cmd.match(scoreboardReadRe());
99
106
  if (readMatch) {
100
107
  const [, temp, player, objective] = readMatch;
101
108
  const key = `${player} ${objective}`;
@@ -110,7 +117,7 @@ function applyLICMInternal(functions) {
110
117
  for (const info of readInfo.values()) {
111
118
  const matches = inner.cmd.match(TEMP_RE) ?? [];
112
119
  const usageCount = matches.filter(name => name === info.temp).length;
113
- const isDef = inner.cmd.startsWith(`execute store result score ${info.temp} rs run scoreboard players get `);
120
+ const isDef = inner.cmd.startsWith(`execute store result score ${info.temp} ${_OBJ_PATTERN} run scoreboard players get `);
114
121
  if (!isDef) {
115
122
  info.uses += usageCount;
116
123
  }
@@ -134,7 +141,7 @@ function applyLICMInternal(functions) {
134
141
  const hoistedTemps = new Set(hoistable.map(item => item.temp));
135
142
  const rewrittenLoopCommands = [];
136
143
  for (const inner of loopFn.commands) {
137
- const readMatch = inner.cmd.match(SCOREBOARD_READ_RE);
144
+ const readMatch = inner.cmd.match(scoreboardReadRe());
138
145
  if (readMatch && hoistedTemps.has(readMatch[1])) {
139
146
  continue;
140
147
  }
@@ -142,7 +149,7 @@ function applyLICMInternal(functions) {
142
149
  }
143
150
  loopFn.commands = rewrittenLoopCommands;
144
151
  nextCommands.push(...hoistable.map(item => ({
145
- cmd: `execute store result score ${item.temp} rs run scoreboard players get ${item.player} ${item.objective}`,
152
+ cmd: `execute store result score ${item.temp} ${_OBJ_PATTERN} run scoreboard players get ${item.player} ${item.objective}`,
146
153
  })), command);
147
154
  stats.licmHoists = (stats.licmHoists ?? 0) + hoistable.length;
148
155
  stats.licmLoopBodies = (stats.licmLoopBodies ?? 0) + 1;
@@ -152,9 +159,9 @@ function applyLICMInternal(functions) {
152
159
  return stats;
153
160
  }
154
161
  function extractArithmeticExpression(commands, index) {
155
- const assign = commands[index]?.cmd.match(/^scoreboard players operation (\$[A-Za-z0-9_]+) rs = (\$[A-Za-z0-9_]+|\$const_-?\d+) rs$/) ??
156
- commands[index]?.cmd.match(/^scoreboard players set (\$[A-Za-z0-9_]+) rs (-?\d+)$/);
157
- const op = commands[index + 1]?.cmd.match(/^scoreboard players operation (\$[A-Za-z0-9_]+) rs ([+\-*/%]=) (\$[A-Za-z0-9_]+|\$const_-?\d+) rs$/);
162
+ const assign = commands[index]?.cmd.match(new RegExp(`^scoreboard players operation (\\$[A-Za-z0-9_]+) ${_OBJ_PATTERN} = (\\$[A-Za-z0-9_]+|\\$const_-?\\d+) ${_OBJ_PATTERN}$`)) ??
163
+ commands[index]?.cmd.match(new RegExp(`^scoreboard players set (\\$[A-Za-z0-9_]+) ${_OBJ_PATTERN} (-?\\d+)$`));
164
+ const op = commands[index + 1]?.cmd.match(new RegExp(`^scoreboard players operation (\\$[A-Za-z0-9_]+) ${_OBJ_PATTERN} ([+\\-*/%]=) (\\$[A-Za-z0-9_]+|\\$const_-?\\d+) ${_OBJ_PATTERN}$`));
158
165
  if (!assign || !op || assign[1] !== op[1]) {
159
166
  return null;
160
167
  }
@@ -184,14 +191,14 @@ function applyCSEInternal(functions) {
184
191
  }
185
192
  for (let i = 0; i < commands.length; i++) {
186
193
  const command = commands[i];
187
- const readMatch = command.cmd.match(SCOREBOARD_READ_RE);
194
+ const readMatch = command.cmd.match(scoreboardReadRe());
188
195
  if (readMatch) {
189
196
  const [, dst, player, objective] = readMatch;
190
197
  const key = `${player} ${objective}`;
191
198
  const cached = readCache.get(key);
192
199
  if (cached) {
193
200
  stats.cseRedundantReads = (stats.cseRedundantReads ?? 0) + 1;
194
- rewritten.push({ ...command, cmd: `scoreboard players operation ${dst} rs = ${cached} rs` });
201
+ rewritten.push({ ...command, cmd: `scoreboard players operation ${dst} ${_OBJ_PATTERN} = ${cached} ${_OBJ_PATTERN}` });
195
202
  }
196
203
  else {
197
204
  readCache.set(key, dst);
@@ -205,7 +212,7 @@ function applyCSEInternal(functions) {
205
212
  if (expr) {
206
213
  const cached = exprCache.get(expr.key);
207
214
  if (cached) {
208
- rewritten.push({ ...commands[i], cmd: `scoreboard players operation ${expr.dst} rs = ${cached} rs` });
215
+ rewritten.push({ ...commands[i], cmd: `scoreboard players operation ${expr.dst} ${_OBJ_PATTERN} = ${cached} ${_OBJ_PATTERN}` });
209
216
  stats.cseArithmetic = (stats.cseArithmetic ?? 0) + 1;
210
217
  i += 1;
211
218
  }
@@ -1,5 +1,6 @@
1
1
  import type { IRCommand, IRFunction } from '../ir/types';
2
2
  import { type OptimizationStats } from './commands';
3
+ export declare function setStructureObjective(obj: string): void;
3
4
  export declare function optimizeFunctionForStructure(fn: IRFunction, functions: Map<string, IRFunction>, namespace: string): IRCommand[];
4
5
  export declare function optimizeForStructure(functions: IRFunction[], namespace?: string): IRFunction[];
5
6
  export declare function optimizeForStructureWithStats(functions: IRFunction[], namespace?: string): {
@@ -1,10 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setStructureObjective = setStructureObjective;
3
4
  exports.optimizeFunctionForStructure = optimizeFunctionForStructure;
4
5
  exports.optimizeForStructure = optimizeForStructure;
5
6
  exports.optimizeForStructureWithStats = optimizeForStructureWithStats;
6
7
  const commands_1 = require("./commands");
7
- const OBJ = 'rs';
8
+ let OBJ = 'rs';
9
+ function setStructureObjective(obj) {
10
+ OBJ = obj;
11
+ (0, commands_1.setOptimizerObjective)(obj);
12
+ }
8
13
  const INLINE_THRESHOLD = 8;
9
14
  const BOP_OP = {
10
15
  '+': '+=',
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "1.0.31",
3
+ "version": "1.0.35",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "redscript-vscode",
9
- "version": "1.0.31",
9
+ "version": "1.0.35",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "redscript": "file:../../"
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "../..": {
25
25
  "name": "redscript-mc",
26
- "version": "1.2.26",
26
+ "version": "1.2.27",
27
27
  "license": "MIT",
28
28
  "bin": {
29
29
  "redscript": "dist/cli.js",
@@ -2,7 +2,7 @@
2
2
  "name": "redscript-vscode",
3
3
  "displayName": "RedScript for Minecraft",
4
4
  "description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
5
- "version": "1.0.31",
5
+ "version": "1.0.35",
6
6
  "publisher": "bkmashiro",
7
7
  "icon": "icon.png",
8
8
  "license": "MIT",
@@ -0,0 +1,146 @@
1
+ // math-showcase.mcrs — Fancy math examples for README / demo
2
+ // Run each fn individually: /function mathshow:<name>
3
+ //
4
+ // Compile: node dist/cli.js compile examples/math-showcase.mcrs -o <dp> --namespace mathshow
5
+
6
+ import "../src/stdlib/math.mcrs"
7
+ import "../src/stdlib/vec.mcrs"
8
+ import "../src/stdlib/advanced.mcrs"
9
+ import "../src/stdlib/bigint.mcrs"
10
+
11
+ // ─── 1. Trig table — print sin & cos for 0°, 15°, 30° … 90° ────────────────
12
+ // NOTE: sin_fixed/cos_fixed return values in ×1000 space (sin(30°) = 500, not 0.5).
13
+ // s*s + c*c ≈ 1000000 because (sin×1000)²+(cos×1000)² = 10⁶×(sin²+cos²) = 10⁶.
14
+ // Requires __load to have run (initializes the sin table).
15
+ fn trig_table() {
16
+ say("§e=== Trig Table (×1000 fixed-point, needs __load) ===");
17
+ let deg: int = 0;
18
+ while (deg <= 90) {
19
+ let s: int = sin_fixed(deg);
20
+ let c: int = cos_fixed(deg);
21
+ say(f" {deg}° sin={s} cos={c} s²+c²={s*s + c*c} (expect ~1000000)");
22
+ deg = deg + 15;
23
+ }
24
+ }
25
+
26
+ // ─── 2. Newton–Raphson sqrt — compare isqrt vs sqrt_fixed ──────────────────
27
+ // isqrt(n) → floor(sqrt(n)), raw integer
28
+ // sqrt_fixed(x) → sqrt of x/1000 expressed in ×1000 format
29
+ // e.g. sqrt_fixed(2000) = sqrt(2.0)×1000 = 1414
30
+ // Input must be in ×1000 space. Max input ≈ 2147000 (before overflow).
31
+ // NOTE: Each isqrt call runs a convergence loop (bits+guess+Newton).
32
+ // Large values = more loop iterations = more MC commands.
33
+ // Keep inputs moderate to stay under Paper's maxCommandChainLength (65536).
34
+ fn sqrt_showcase() {
35
+ say("§e=== Integer Square Root (isqrt) ===");
36
+ say(f" isqrt(2) = {isqrt(2)} (exact: 1)");
37
+ say(f" isqrt(144) = {isqrt(144)} (exact: 12)");
38
+ say(f" isqrt(10000) = {isqrt(10000)} (exact: 100)");
39
+ say(f" isqrt(1000000) = {isqrt(1000000)} (exact: 1000)");
40
+
41
+ say("§e=== Fixed-Point sqrt (input&output in ×1000) ===");
42
+ say(f" sqrt_fixed(1000) = {sqrt_fixed(1000)} (sqrt(1.0) = 1.000)");
43
+ say(f" sqrt_fixed(2000) = {sqrt_fixed(2000)} (sqrt(2.0) ≈ 1.414)");
44
+ say(f" sqrt_fixed(4000) = {sqrt_fixed(4000)} (sqrt(4.0) = 2.000)");
45
+ say(f" sqrt_fixed(9000) = {sqrt_fixed(9000)} (sqrt(9.0) = 3.000)");
46
+ }
47
+
48
+ // ─── 3. 2D vector geometry ──────────────────────────────────────────────────
49
+ fn vector_showcase() {
50
+ say("§e=== 2D Vector Geometry ===");
51
+ // 3-4-5 right triangle
52
+ let d345: int = distance2d_fixed(0, 0, 300, 400);
53
+ say(f" dist((0,0)→(300,400)) = {d345} (expect 500)");
54
+ // unit circle
55
+ let d_unit: int = length2d_fixed(1000, 0);
56
+ say(f" length(1000, 0) = {d_unit} (expect 1000)");
57
+ // atan2
58
+ say(f" atan2(1000, 0) = {atan2_fixed(1000, 0)}° (expect 90)");
59
+ say(f" atan2(1000, 1000) = {atan2_fixed(1000, 1000)}° (expect 45)");
60
+ say(f" atan2(-1, 0) = {atan2_fixed(-1, 0)}° (expect 270)");
61
+ // dot product: orthogonal vectors → 0
62
+ say(f" dot2d(1000,0, 0,1000) = {dot2d(1000, 0, 0, 1000)} (expect 0, orthogonal)");
63
+ say(f" dot2d(1000,0, 1000,0) = {dot2d(1000, 0, 1000, 0)} (expect 1000000, parallel)");
64
+ }
65
+
66
+ // ─── 4. Number theory ───────────────────────────────────────────────────────
67
+ fn number_theory() {
68
+ say("§e=== Number Theory ===");
69
+ say(f" gcd(360, 252) = {gcd(360, 252)} (expect 36)");
70
+ say(f" lcm(12, 18) = {lcm(12, 18)} (expect 36)");
71
+ say(f" is_prime(97) = {is_prime(97)} (expect 1)");
72
+ say(f" is_prime(100) = {is_prime(100)} (expect 0)");
73
+ say(f" fib(20) = {fib(20)} (expect 6765)");
74
+ say(f" collatz_steps(27) = {collatz_steps(27)} (expect 111)");
75
+ say(f" digit_sum(123456789) = {digit_sum(123456789)} (expect 45)");
76
+ say(f" mod_pow(2, 10, 1000) = {mod_pow(2, 10, 1000)} (expect 24)");
77
+ say(f" mod_pow(7, 5, 13) = {mod_pow(7, 5, 13)} (expect 11)");
78
+ }
79
+
80
+ // ─── 5. Fixed-point interpolation & easing ──────────────────────────────────
81
+ fn easing_showcase() {
82
+ say("§e=== Easing & Interpolation (t = 0..1000) ===");
83
+ let t: int = 0;
84
+ while (t <= 1000) {
85
+ let lin: int = lerp(0, 1000, t);
86
+ let sm: int = smoothstep(0, 1000, t);
87
+ let smr: int = smootherstep(0, 1000, t);
88
+ say(f" t={t} lerp={lin} smooth={sm} smoother={smr}");
89
+ t = t + 250;
90
+ }
91
+ }
92
+
93
+ // ─── 6. Hash & noise ────────────────────────────────────────────────────────
94
+ fn hash_showcase() {
95
+ say("§e=== Hash & Procedural Noise ===");
96
+ say(f" hash_int(0) = {hash_int(0)}");
97
+ say(f" hash_int(1) = {hash_int(1)}");
98
+ say(f" hash_int(42) = {hash_int(42)}");
99
+ say(f" noise1d(0) = {noise1d(0)} (0..1000)");
100
+ say(f" noise1d(100) = {noise1d(100)}");
101
+ say(f" noise1d(200) = {noise1d(200)}");
102
+ say(f" noise1d(300) = {noise1d(300)}");
103
+ }
104
+
105
+ // ─── 7. BigInt Fibonacci — numbers beyond INT32 ─────────────────────────────
106
+ fn bigint_showcase() {
107
+ say("§e=== BigInt — Arbitrary Precision (base 10000, 8 limbs) ===");
108
+ bigint_fib(50);
109
+ let l0: int = bigint_get_a(0);
110
+ let l1: int = bigint_get_a(1);
111
+ let l2: int = bigint_get_a(2);
112
+ say(f" fib(50) limbs: [{l2}][{l1}][{l0}]");
113
+ say(f" = {l2}×10⁸ + {l1}×10⁴ + {l0}");
114
+ say(f" = 12,586,269,025 (verify: l0=9025 l1=8626 l2=125)");
115
+
116
+ bigint_fib(70);
117
+ let m0: int = bigint_get_a(0);
118
+ let m1: int = bigint_get_a(1);
119
+ let m2: int = bigint_get_a(2);
120
+ let m3: int = bigint_get_a(3);
121
+ say(f" fib(70) limbs: [{m3}][{m2}][{m1}][{m0}]");
122
+ say(f" = 190,392,490,709,135");
123
+ }
124
+
125
+ // ─── 8. Mandelbrot / Julia — fractals in integer arithmetic ─────────────────
126
+ fn fractal_showcase() {
127
+ say("§e=== Mandelbrot & Julia (fixed-point, scale=1000) ===");
128
+ // Mandelbrot: does (cr, ci) escape in <max_iter> steps?
129
+ say(f" mandelbrot_iter(0, 0, 64) = {mandelbrot_iter(0, 0, 64)} (inside, expect 64)");
130
+ say(f" mandelbrot_iter(2000, 0, 64) = {mandelbrot_iter(2000, 0, 64)} (outside, < 64)");
131
+ say(f" mandelbrot_iter(-500, 500, 64) = {mandelbrot_iter(-500, 500, 64)}");
132
+ say(f" julia_iter(0, 0, -500, 250, 64) = {julia_iter(0, 0, -500, 250, 64)}");
133
+ say(f" julia_iter(500, 0, -500, 250, 64)= {julia_iter(500, 0, -500, 250, 64)}");
134
+ }
135
+
136
+ // ─── 9. Run everything ──────────────────────────────────────────────────────
137
+ fn run_all() {
138
+ trig_table();
139
+ sqrt_showcase();
140
+ vector_showcase();
141
+ number_theory();
142
+ easing_showcase();
143
+ hash_showcase();
144
+ bigint_showcase();
145
+ fractal_showcase();
146
+ }
@@ -0,0 +1,92 @@
1
+ // readme-demo.mcrs — RedScript v1.2.27 README visual showcase
2
+ // What 55 lines of RedScript can do on a vanilla Minecraft server.
3
+ //
4
+ // Compile: node dist/cli.js compile examples/readme-demo.mcrs -o <datapack> --namespace rsdemo
5
+ // In-game: /function rsdemo:start (then record your gif)
6
+ // /function rsdemo:stop
7
+
8
+ import "../src/stdlib/math.mcrs"
9
+
10
+ let t: int = 0; // 0..359 degrees, 4° per tick = full cycle every 1.5 s
11
+ let frame: int = 0;
12
+ let on: bool = false;
13
+
14
+ @load fn _init() {
15
+ t = 0;
16
+ frame = 0;
17
+ }
18
+
19
+ @tick fn _wave_tick() {
20
+ if (!on) { return; }
21
+
22
+ t = (t + 4) % 360;
23
+ frame = frame + 1;
24
+
25
+ let s: int = sin_fixed(t); // -1000 .. 1000
26
+ let c: int = cos_fixed(t); // -1000 .. 1000
27
+ let mag: int = abs(s); // 0 .. 1000
28
+
29
+ // ── Particle rings: spread values scale with sin(t) ─────────────────
30
+ // Inner ring — always visible, small tight shimmer
31
+ foreach (p in @a) at @s {
32
+ particle("minecraft:end_rod", ~0, ~1, ~0, 0.25, 0.08, 0.25, 0.02, 6);
33
+ }
34
+
35
+ // Mid ring — appears when sin > 0 (upper half of cycle)
36
+ if (s > 0) {
37
+ foreach (p in @a) at @s {
38
+ particle("minecraft:end_rod", ~0, ~1, ~0, 0.9, 0.25, 0.9, 0.02, 12);
39
+ }
40
+ }
41
+
42
+ // Outer ring + soul fire — sin > 600
43
+ if (s > 600) {
44
+ foreach (p in @a) at @s {
45
+ particle("minecraft:end_rod", ~0, ~1, ~0, 1.8, 0.4, 1.8, 0.02, 18);
46
+ particle("minecraft:soul_fire_flame", ~0, ~2, ~0, 0.9, 0.3, 0.9, 0.03, 8);
47
+ }
48
+ }
49
+
50
+ // Peak flash — fires ~4 ticks per cycle
51
+ if (s > 940) {
52
+ foreach (p in @a) at @s {
53
+ particle("minecraft:flash", ~0, ~3, ~0, 0.4, 0.3, 0.4, 0.1, 3);
54
+ }
55
+ }
56
+
57
+ // Negative trough — ground portal swirl
58
+ if (s < -700) {
59
+ foreach (p in @a) at @s {
60
+ particle("minecraft:portal", ~0, ~0.5, ~0, 1.2, 0.4, 1.2, 0.05, 12);
61
+ }
62
+ }
63
+
64
+ // ── Actionbar: live math rotating every 3 s ──────────────────────────
65
+ let seg: int = (frame / 45) % 4;
66
+ if (seg == 0) {
67
+ actionbar(@a, f"§3⚡ §bsin({t}°) = §e{s}‰ cos({t}°) = §e{c}‰");
68
+ }
69
+ if (seg == 1) {
70
+ actionbar(@a, f"§3⚡ §bsmoothstep(|sin|) = §e{smoothstep(0, 1000, mag)}‰ isqrt({frame}) = §e{isqrt(frame)}");
71
+ }
72
+ if (seg == 2) {
73
+ actionbar(@a, f"§3⚡ §bgcd(360, {t + 1}) = §e{gcd(360, t + 1)} log2({mag + 1}) = §e{log2_int(mag + 1)}");
74
+ }
75
+ if (seg == 3) {
76
+ actionbar(@a, f"§3⚡ §bpow(2, {(frame / 30) % 10 + 1}) = §e{pow_int(2, (frame / 30) % 10 + 1)} clamp(sin) = §e{clamp(s, -500, 500)}");
77
+ }
78
+ }
79
+
80
+ fn start() {
81
+ on = true;
82
+ t = 0;
83
+ frame = 0;
84
+ title(@a, f"§e✦ §bRedScript§e ✦", f"§7trig-driven particle rings");
85
+ say("§a▶ readme-demo started — /function rsdemo:stop to end");
86
+ }
87
+
88
+ fn stop() {
89
+ on = false;
90
+ actionbar(@a, f"§7[ demo stopped ]");
91
+ say("§c■ readme-demo stopped");
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.2.27",
3
+ "version": "1.2.28",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -55,7 +55,7 @@ describe('CLI API', () => {
55
55
  const result = compile(source, { namespace: 'mygame', filePath: mainPath })
56
56
  const tickTimer = result.files.find(file => file.path.endsWith('/tick_timer.mcfunction'))
57
57
 
58
- expect(tickTimer?.content).toContain('scoreboard players set #rs rs.timer_ticks 1')
58
+ expect(tickTimer?.content).toContain('scoreboard players set #rs mygame.timer_ticks 1')
59
59
  })
60
60
 
61
61
  it('adds a call-site hash for stdlib internal scoreboard objectives', () => {
@@ -89,7 +89,7 @@ describe('CLI API', () => {
89
89
  expect(timerFns).toHaveLength(2)
90
90
 
91
91
  const objectives = timerFns
92
- .flatMap(file => [...file.content.matchAll(/rs\._timer_([0-9a-f]{4})/g)].map(match => match[0]))
92
+ .flatMap(file => [...file.content.matchAll(/mygame\._timer_([0-9a-f]{4})/g)].map(match => match[0]))
93
93
 
94
94
  expect(new Set(objectives).size).toBe(2)
95
95
  })
@@ -113,8 +113,8 @@ describe('CLI API', () => {
113
113
 
114
114
  expect(result.typeErrors).toEqual([])
115
115
  const newFn = result.files.find(file => file.path.endsWith('/Timer_new.mcfunction'))
116
- expect(newFn?.content).toContain('scoreboard players set timer_ticks rs 0')
117
- expect(newFn?.content).toContain('scoreboard players set timer_active rs 0')
116
+ expect(newFn?.content).toContain('scoreboard players set timer_ticks timernew 0')
117
+ expect(newFn?.content).toContain('scoreboard players set timer_active timernew 0')
118
118
  })
119
119
 
120
120
  it('Timer.start/pause/reset', () => {
@@ -142,9 +142,9 @@ describe('CLI API', () => {
142
142
  const pauseFn = result.files.find(file => file.path.endsWith('/Timer_pause.mcfunction'))
143
143
  const resetFn = result.files.find(file => file.path.endsWith('/Timer_reset.mcfunction'))
144
144
 
145
- expect(startFn?.content).toContain('scoreboard players set timer_active rs 1')
146
- expect(pauseFn?.content).toContain('scoreboard players set timer_active rs 0')
147
- expect(resetFn?.content).toContain('scoreboard players set timer_ticks rs 0')
145
+ expect(startFn?.content).toContain('scoreboard players set timer_active timerstate 1')
146
+ expect(pauseFn?.content).toContain('scoreboard players set timer_active timerstate 0')
147
+ expect(resetFn?.content).toContain('scoreboard players set timer_ticks timerstate 0')
148
148
  })
149
149
 
150
150
  it('Timer.done returns bool', () => {
@@ -171,9 +171,9 @@ describe('CLI API', () => {
171
171
  expect(result.typeErrors).toEqual([])
172
172
  const doneFn = result.files.find(file => file.path.endsWith('/Timer_done.mcfunction'))
173
173
  const mainFn = result.files.find(file => file.path.endsWith('/main.mcfunction'))
174
- expect(doneFn?.content).toContain('scoreboard players get timer_ticks rs')
174
+ expect(doneFn?.content).toContain('scoreboard players get timer_ticks timerdone')
175
175
  expect(doneFn?.content).toContain('return run scoreboard players get')
176
- expect(mainFn?.content).toContain('execute if score $main_finished rs matches 1..')
176
+ expect(mainFn?.content).toContain('execute if score $main_finished timerdone matches 1..')
177
177
  })
178
178
 
179
179
  it('Timer.tick increments', () => {
@@ -201,10 +201,10 @@ describe('CLI API', () => {
201
201
  .map(file => file.content)
202
202
  .join('\n')
203
203
 
204
- expect(tickOutput).toContain('scoreboard players get timer_active rs')
205
- expect(tickOutput).toContain('scoreboard players get timer_ticks rs')
206
- expect(tickOutput).toContain(' += $const_1 rs')
207
- expect(tickOutput).toContain('execute store result score timer_ticks rs run scoreboard players get $_')
204
+ expect(tickOutput).toContain('scoreboard players get timer_active timertick')
205
+ expect(tickOutput).toContain('scoreboard players get timer_ticks timertick')
206
+ expect(tickOutput).toContain(' += $const_1 timertick')
207
+ expect(tickOutput).toContain('execute store result score timer_ticks timertick run scoreboard players get $_')
208
208
  })
209
209
  })
210
210
 
@@ -28,7 +28,7 @@ fn turret_tick() {
28
28
  const parent = getFileContent(result.files, 'data/test/function/turret_tick.mcfunction')
29
29
  const loopBody = getFileContent(result.files, 'data/test/function/turret_tick/foreach_0.mcfunction')
30
30
 
31
- const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config test.turret_range'
31
+ const hoistedRead = 'execute store result score $_0 test run scoreboard players get config test.turret_range'
32
32
  const executeCall = 'execute as @e[tag=turret] run function test:turret_tick/foreach_0'
33
33
 
34
34
  expect(parent).toContain(hoistedRead)
@@ -54,7 +54,7 @@ fn read_twice() {
54
54
  const readMatches = fn.match(/scoreboard players get @s test\.coins/g) ?? []
55
55
 
56
56
  expect(readMatches).toHaveLength(1)
57
- expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs')
57
+ expect(fn).toContain('scoreboard players operation $_1 test = $_0 test')
58
58
  })
59
59
 
60
60
  test('reuses duplicate arithmetic sequences', () => {
@@ -71,10 +71,10 @@ fn math() {
71
71
 
72
72
  const result = compile(source, { namespace: 'test' })
73
73
  const fn = getFileContent(result.files, 'data/test/function/math.mcfunction')
74
- const addMatches = fn.match(/\+= \$const_2 rs/g) ?? []
74
+ const addMatches = fn.match(/\+= \$const_2 test/g) ?? []
75
75
 
76
76
  expect(addMatches).toHaveLength(1)
77
- expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs')
77
+ expect(fn).toContain('scoreboard players operation $_1 test = $_0 test')
78
78
  })
79
79
  })
80
80
 
package/src/cli.ts CHANGED
@@ -29,7 +29,7 @@ function printUsage(): void {
29
29
  RedScript Compiler
30
30
 
31
31
  Usage:
32
- redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>] [--no-dce]
32
+ redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--scoreboard <obj>] [--target <target>] [--no-dce]
33
33
  redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
34
34
  redscript check <file>
35
35
  redscript fmt <file.mcrs> [file2.mcrs ...]
@@ -54,6 +54,9 @@ Options:
54
54
  --target <target> Output target: datapack (default), cmdblock, or structure
55
55
  --no-dce Disable AST dead code elimination
56
56
  --no-mangle Disable variable name mangling (use readable names)
57
+ --scoreboard <obj> Scoreboard objective for variables (default: namespace).
58
+ Each datapack automatically uses its namespace as the objective
59
+ so multiple datapacks can coexist without collisions.
57
60
  --stats Print optimizer statistics
58
61
  --hot-reload <url> After each successful compile, POST to <url>/reload
59
62
  (use with redscript-testharness; e.g. http://localhost:25561)
@@ -168,6 +171,7 @@ function parseArgs(args: string[]): {
168
171
  hotReload?: string
169
172
  dce?: boolean
170
173
  mangle?: boolean
174
+ scoreboardObjective?: string
171
175
  } {
172
176
  const result: ReturnType<typeof parseArgs> = { dce: true, mangle: true }
173
177
  let i = 0
@@ -199,6 +203,9 @@ function parseArgs(args: string[]): {
199
203
  } else if (arg === '--no-mangle') {
200
204
  result.mangle = false
201
205
  i++
206
+ } else if (arg === '--scoreboard') {
207
+ result.scoreboardObjective = args[++i]
208
+ i++
202
209
  } else if (arg === '--hot-reload') {
203
210
  result.hotReload = args[++i]
204
211
  i++
@@ -262,7 +269,8 @@ function compileCommand(
262
269
  target: string = 'datapack',
263
270
  showStats = false,
264
271
  dce = true,
265
- mangle = true
272
+ mangle = true,
273
+ scoreboardObjective: string | undefined = undefined
266
274
  ): void {
267
275
  // Read source file
268
276
  if (!fs.existsSync(file)) {
@@ -274,7 +282,7 @@ function compileCommand(
274
282
 
275
283
  try {
276
284
  if (target === 'cmdblock') {
277
- const result = compile(source, { namespace, filePath: file, dce, mangle })
285
+ const result = compile(source, { namespace, filePath: file, dce, mangle, scoreboardObjective })
278
286
  printWarnings(result.warnings)
279
287
 
280
288
  // Generate command block JSON
@@ -305,7 +313,7 @@ function compileCommand(
305
313
  printOptimizationStats(structure.stats)
306
314
  }
307
315
  } else {
308
- const result = compile(source, { namespace, filePath: file, dce, mangle })
316
+ const result = compile(source, { namespace, filePath: file, dce, mangle, scoreboardObjective })
309
317
  printWarnings(result.warnings)
310
318
 
311
319
  // Default: generate datapack
@@ -508,7 +516,8 @@ async function main(): Promise<void> {
508
516
  target,
509
517
  parsed.stats,
510
518
  parsed.dce,
511
- parsed.mangle
519
+ parsed.mangle,
520
+ parsed.scoreboardObjective // undefined = derive from namespace in compile()
512
521
  )
513
522
  }
514
523
  break
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import type { IRBlock, IRFunction, IRInstr, IRModule, Operand, Terminator } from '../../ir/types'
20
- import { optimizeCommandFunctions, type OptimizationStats, createEmptyOptimizationStats, mergeOptimizationStats } from '../../optimizer/commands'
20
+ import { optimizeCommandFunctions, setOptimizerObjective, type OptimizationStats, createEmptyOptimizationStats, mergeOptimizationStats } from '../../optimizer/commands'
21
21
  import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
22
22
  import { VarAllocator } from '../var-allocator'
23
23
 
@@ -25,7 +25,8 @@ import { VarAllocator } from '../var-allocator'
25
25
  // Utilities
26
26
  // ---------------------------------------------------------------------------
27
27
 
28
- const OBJ = 'rs' // scoreboard objective name
28
+ // Default scoreboard objective — overridden per-compilation via DatapackGenerationOptions.scoreboardObjective
29
+ let OBJ = 'rs'
29
30
 
30
31
  function operandToScore(op: Operand, alloc: VarAllocator): string {
31
32
  if (op.kind === 'var') return `${alloc.alloc(op.name)} ${OBJ}`
@@ -284,6 +285,10 @@ export interface DatapackGenerationResult {
284
285
  export interface DatapackGenerationOptions {
285
286
  optimizeCommands?: boolean
286
287
  mangle?: boolean
288
+ /** Scoreboard objective used for all scoreboard variables.
289
+ * Defaults to 'rs'. Override per-datapack to avoid collisions
290
+ * when multiple RedScript datapacks are loaded simultaneously. */
291
+ scoreboardObjective?: string
287
292
  }
288
293
 
289
294
  export function countMcfunctionCommands(files: DatapackFile[]): number {
@@ -354,7 +359,11 @@ export function generateDatapackWithStats(
354
359
  module: IRModule,
355
360
  options: DatapackGenerationOptions = {},
356
361
  ): DatapackGenerationResult {
357
- const { optimizeCommands = true, mangle = false } = options
362
+ const { optimizeCommands = true, mangle = false, scoreboardObjective = 'rs' } = options
363
+ // Set module-level OBJ so all helper functions in this module use the correct objective.
364
+ // This is safe because compilation is synchronous.
365
+ OBJ = scoreboardObjective
366
+ setOptimizerObjective(scoreboardObjective)
358
367
  const alloc = new VarAllocator(mangle)
359
368
  const files: DatapackFile[] = []
360
369
  const advancements: DatapackFile[] = []
@@ -402,9 +411,9 @@ export function generateDatapackWithStats(
402
411
  for (const eventType of eventTypes) {
403
412
  const detection = EVENT_TYPES[eventType].detection
404
413
  if (eventType === 'PlayerDeath') {
405
- loadLines.push('scoreboard objectives add rs.deaths deathCount')
414
+ loadLines.push(`scoreboard objectives add ${OBJ}.deaths deathCount`)
406
415
  } else if (eventType === 'EntityKill') {
407
- loadLines.push('scoreboard objectives add rs.kills totalKillCount')
416
+ loadLines.push(`scoreboard objectives add ${OBJ}.kills totalKillCount`)
408
417
  } else if (eventType === 'ItemUse') {
409
418
  loadLines.push('# ItemUse detection requires a project-specific objective/tag setup')
410
419
  } else if (detection === 'tag' || detection === 'advancement') {