redscript-mc 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +164 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +8 -8
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +20 -1
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.js +25 -0
- package/dist/compile.d.ts +10 -0
- package/dist/compile.js +36 -5
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/index.js +3 -2
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +439 -15
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +79 -10
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/out/extension.js +1144 -72
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -0
- package/src/__tests__/e2e.test.ts +201 -12
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +14 -2
- package/src/__tests__/lowering.test.ts +178 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +8 -8
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +25 -1
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +27 -0
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- package/src/index.ts +4 -3
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +528 -16
- package/src/parser/index.ts +90 -12
- package/src/stdlib/README.md +34 -4
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/timer.mcrs +54 -33
- package/src/typechecker/index.ts +404 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RedScript will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.2.0] - 2026-03-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `is` type narrowing for entity checks (`if (e is Player)`)
|
|
9
|
+
- `impl` blocks for struct methods
|
|
10
|
+
- Static method calls (`Type::method()`)
|
|
11
|
+
- Timer OOP API in stdlib
|
|
12
|
+
- `setTimeout(delay, callback)` builtin
|
|
13
|
+
- `setInterval(delay, callback)` builtin
|
|
14
|
+
- `clearInterval(id)` builtin
|
|
15
|
+
- `@on(Event)` static event system
|
|
16
|
+
- PlayerDeath, PlayerJoin, BlockBreak, EntityKill, ItemUse
|
|
17
|
+
- Automatic namespace prefixing for scoreboard objectives
|
|
18
|
+
- Comprehensive MC tag constants (313 tags)
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Stdlib timer functions now use OOP API
|
|
22
|
+
|
|
23
|
+
## [1.1.0] - 2026-03-12
|
|
24
|
+
|
|
25
|
+
### Language Features
|
|
26
|
+
- **Variable selector syntax**: `execute if entity p[x_rotation=-90..-45]` now works in foreach loops
|
|
27
|
+
- **New selector filters**: `x_rotation`, `y_rotation`, `x`, `y`, `z` for rotation and position checks
|
|
28
|
+
- **Duplicate binding detection**: Error when redeclaring foreach variables
|
|
29
|
+
|
|
30
|
+
### Builtins
|
|
31
|
+
- `effect_clear(target, [effect])` — Clear all or specific effects
|
|
32
|
+
- `data_merge(target, nbt)` — Merge NBT data into entities
|
|
33
|
+
|
|
34
|
+
### Standard Library
|
|
35
|
+
- `effects.mcrs` — Effect shortcuts (speed, strength, regen, buff_all...)
|
|
36
|
+
- `world.mcrs` — World/gamerule helpers (set_day, weather_clear, enable_keep_inventory...)
|
|
37
|
+
- `inventory.mcrs` — Inventory management (give_kit_warrior, clear_inventory...)
|
|
38
|
+
- `particles.mcrs` — Particle effects (hearts_at, flames, sparkles_at...)
|
|
39
|
+
- `spawn.mcrs` — Teleport utilities (teleport_to, gather_all, goto_lobby...)
|
|
40
|
+
- `teams.mcrs` — Team management (create_red_team, setup_two_teams...)
|
|
41
|
+
- `bossbar.mcrs` — Bossbar helpers (create_progress_bar, update_bar...)
|
|
42
|
+
- `interactions.mcrs` — Input detection (check_look_up, on_right_click, on_sneak_click...)
|
|
43
|
+
|
|
44
|
+
### Bug Fixes
|
|
45
|
+
- Negative coordinates in summon/tp/particle now work correctly
|
|
46
|
+
- Stdlib particles use coordinates instead of selectors
|
|
47
|
+
|
|
48
|
+
### Documentation
|
|
49
|
+
- Added tutorials: Zombie Survival, Capture the Flag, Parkour Race
|
|
50
|
+
- Added local debugging guide
|
|
51
|
+
- Added stdlib reference page
|
|
52
|
+
- Added Paper server testing guide
|
|
53
|
+
|
|
54
|
+
### Community
|
|
55
|
+
- CONTRIBUTING.md with development guide
|
|
56
|
+
- GitHub issue/PR templates
|
|
57
|
+
- CHANGELOG.md
|
|
58
|
+
|
|
5
59
|
## [1.0.0] - 2026-03-12
|
|
6
60
|
|
|
7
61
|
### 🎉 Initial Release
|
|
@@ -66,6 +66,144 @@ describe('CLI API', () => {
|
|
|
66
66
|
expect(result.ir.functions.filter(fn => fn.name === 'from_a')).toHaveLength(1);
|
|
67
67
|
expect(result.ir.functions.filter(fn => fn.name === 'from_b')).toHaveLength(1);
|
|
68
68
|
});
|
|
69
|
+
it('uses rs-prefixed scoreboard objectives for imported stdlib files', () => {
|
|
70
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-'));
|
|
71
|
+
const stdlibDir = path.join(tempDir, 'src', 'stdlib');
|
|
72
|
+
const stdlibPath = path.join(stdlibDir, 'timer.mcrs');
|
|
73
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
74
|
+
fs.mkdirSync(stdlibDir, { recursive: true });
|
|
75
|
+
fs.writeFileSync(stdlibPath, 'fn tick_timer() { scoreboard_set("#rs", "timer_ticks", 1); }\n');
|
|
76
|
+
fs.writeFileSync(mainPath, 'import "./src/stdlib/timer.mcrs"\n\nfn main() { tick_timer(); }\n');
|
|
77
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
78
|
+
const result = (0, index_1.compile)(source, { namespace: 'mygame', filePath: mainPath });
|
|
79
|
+
const tickTimer = result.files.find(file => file.path.endsWith('/tick_timer.mcfunction'));
|
|
80
|
+
expect(tickTimer?.content).toContain('scoreboard players set #rs rs.timer_ticks 1');
|
|
81
|
+
});
|
|
82
|
+
it('adds a call-site hash for stdlib internal scoreboard objectives', () => {
|
|
83
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-hash-'));
|
|
84
|
+
const stdlibDir = path.join(tempDir, 'src', 'stdlib');
|
|
85
|
+
const stdlibPath = path.join(stdlibDir, 'timer.mcrs');
|
|
86
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
87
|
+
fs.mkdirSync(stdlibDir, { recursive: true });
|
|
88
|
+
fs.writeFileSync(stdlibPath, [
|
|
89
|
+
'fn timer_start(name: string, duration: int) {',
|
|
90
|
+
' scoreboard_set("timer_ticks", #rs, duration);',
|
|
91
|
+
' scoreboard_set("timer_active", #rs, 1);',
|
|
92
|
+
'}',
|
|
93
|
+
'',
|
|
94
|
+
].join('\n'));
|
|
95
|
+
fs.writeFileSync(mainPath, [
|
|
96
|
+
'import "./src/stdlib/timer.mcrs"',
|
|
97
|
+
'',
|
|
98
|
+
'fn main() {',
|
|
99
|
+
' timer_start("x", 100);',
|
|
100
|
+
' timer_start("x", 100);',
|
|
101
|
+
'}',
|
|
102
|
+
'',
|
|
103
|
+
].join('\n'));
|
|
104
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
105
|
+
const result = (0, index_1.compile)(source, { namespace: 'mygame', filePath: mainPath });
|
|
106
|
+
const timerFns = result.files.filter(file => /timer_start__callsite_[0-9a-f]{4}\.mcfunction$/.test(file.path));
|
|
107
|
+
expect(timerFns).toHaveLength(2);
|
|
108
|
+
const objectives = timerFns
|
|
109
|
+
.flatMap(file => [...file.content.matchAll(/rs\._timer_([0-9a-f]{4})/g)].map(match => match[0]));
|
|
110
|
+
expect(new Set(objectives).size).toBe(2);
|
|
111
|
+
});
|
|
112
|
+
it('Timer::new creates timer', () => {
|
|
113
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-new-'));
|
|
114
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
115
|
+
const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs');
|
|
116
|
+
fs.writeFileSync(mainPath, [
|
|
117
|
+
`import "${timerPath}"`,
|
|
118
|
+
'',
|
|
119
|
+
'fn main() {',
|
|
120
|
+
' let timer: Timer = Timer::new(20);',
|
|
121
|
+
'}',
|
|
122
|
+
'',
|
|
123
|
+
].join('\n'));
|
|
124
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
125
|
+
const result = (0, index_1.compile)(source, { namespace: 'timernew', filePath: mainPath });
|
|
126
|
+
expect(result.typeErrors).toEqual([]);
|
|
127
|
+
const newFn = result.files.find(file => file.path.endsWith('/Timer_new.mcfunction'));
|
|
128
|
+
expect(newFn?.content).toContain('scoreboard players set timer_ticks rs 0');
|
|
129
|
+
expect(newFn?.content).toContain('scoreboard players set timer_active rs 0');
|
|
130
|
+
});
|
|
131
|
+
it('Timer.start/pause/reset', () => {
|
|
132
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-state-'));
|
|
133
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
134
|
+
const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs');
|
|
135
|
+
fs.writeFileSync(mainPath, [
|
|
136
|
+
`import "${timerPath}"`,
|
|
137
|
+
'',
|
|
138
|
+
'fn main() {',
|
|
139
|
+
' let timer: Timer = Timer::new(20);',
|
|
140
|
+
' timer.start();',
|
|
141
|
+
' timer.pause();',
|
|
142
|
+
' timer.reset();',
|
|
143
|
+
'}',
|
|
144
|
+
'',
|
|
145
|
+
].join('\n'));
|
|
146
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
147
|
+
const result = (0, index_1.compile)(source, { namespace: 'timerstate', filePath: mainPath });
|
|
148
|
+
expect(result.typeErrors).toEqual([]);
|
|
149
|
+
const startFn = result.files.find(file => file.path.endsWith('/Timer_start.mcfunction'));
|
|
150
|
+
const pauseFn = result.files.find(file => file.path.endsWith('/Timer_pause.mcfunction'));
|
|
151
|
+
const resetFn = result.files.find(file => file.path.endsWith('/Timer_reset.mcfunction'));
|
|
152
|
+
expect(startFn?.content).toContain('scoreboard players set timer_active rs 1');
|
|
153
|
+
expect(pauseFn?.content).toContain('scoreboard players set timer_active rs 0');
|
|
154
|
+
expect(resetFn?.content).toContain('scoreboard players set timer_ticks rs 0');
|
|
155
|
+
});
|
|
156
|
+
it('Timer.done returns bool', () => {
|
|
157
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-done-'));
|
|
158
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
159
|
+
const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs');
|
|
160
|
+
fs.writeFileSync(mainPath, [
|
|
161
|
+
`import "${timerPath}"`,
|
|
162
|
+
'',
|
|
163
|
+
'fn main() {',
|
|
164
|
+
' let timer: Timer = Timer::new(20);',
|
|
165
|
+
' let finished: bool = timer.done();',
|
|
166
|
+
' if (finished) {',
|
|
167
|
+
' say("done");',
|
|
168
|
+
' }',
|
|
169
|
+
'}',
|
|
170
|
+
'',
|
|
171
|
+
].join('\n'));
|
|
172
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
173
|
+
const result = (0, index_1.compile)(source, { namespace: 'timerdone', filePath: mainPath });
|
|
174
|
+
expect(result.typeErrors).toEqual([]);
|
|
175
|
+
const doneFn = result.files.find(file => file.path.endsWith('/Timer_done.mcfunction'));
|
|
176
|
+
const mainFn = result.files.find(file => file.path.endsWith('/main.mcfunction'));
|
|
177
|
+
expect(doneFn?.content).toContain('scoreboard players get timer_ticks rs');
|
|
178
|
+
expect(doneFn?.content).toContain('return run scoreboard players get');
|
|
179
|
+
expect(mainFn?.content).toContain('execute if score $finished rs matches 1..');
|
|
180
|
+
});
|
|
181
|
+
it('Timer.tick increments', () => {
|
|
182
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-tick-'));
|
|
183
|
+
const mainPath = path.join(tempDir, 'main.mcrs');
|
|
184
|
+
const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs');
|
|
185
|
+
fs.writeFileSync(mainPath, [
|
|
186
|
+
`import "${timerPath}"`,
|
|
187
|
+
'',
|
|
188
|
+
'fn main() {',
|
|
189
|
+
' let timer: Timer = Timer::new(20);',
|
|
190
|
+
' timer.start();',
|
|
191
|
+
' timer.tick();',
|
|
192
|
+
'}',
|
|
193
|
+
'',
|
|
194
|
+
].join('\n'));
|
|
195
|
+
const source = fs.readFileSync(mainPath, 'utf-8');
|
|
196
|
+
const result = (0, index_1.compile)(source, { namespace: 'timertick', filePath: mainPath });
|
|
197
|
+
expect(result.typeErrors).toEqual([]);
|
|
198
|
+
const tickOutput = result.files
|
|
199
|
+
.filter(file => file.path.includes('/Timer_tick'))
|
|
200
|
+
.map(file => file.content)
|
|
201
|
+
.join('\n');
|
|
202
|
+
expect(tickOutput).toContain('scoreboard players get timer_active rs');
|
|
203
|
+
expect(tickOutput).toContain('scoreboard players get timer_ticks rs');
|
|
204
|
+
expect(tickOutput).toContain(' += $const_1 rs');
|
|
205
|
+
expect(tickOutput).toContain('execute store result score timer_ticks rs run scoreboard players get $_');
|
|
206
|
+
});
|
|
69
207
|
});
|
|
70
208
|
describe('compile()', () => {
|
|
71
209
|
it('compiles simple source', () => {
|
|
@@ -117,5 +117,30 @@ describe('generateDatapack', () => {
|
|
|
117
117
|
expect(json.criteria.trigger.trigger).toBe('minecraft:story/mine_diamond');
|
|
118
118
|
expect(json.rewards.function).toBe('mypack:on_mine_diamond');
|
|
119
119
|
});
|
|
120
|
+
it('generates static event dispatcher in __tick', () => {
|
|
121
|
+
const mod = {
|
|
122
|
+
namespace: 'mypack',
|
|
123
|
+
globals: [],
|
|
124
|
+
functions: [{
|
|
125
|
+
name: 'handle_death',
|
|
126
|
+
params: [],
|
|
127
|
+
locals: [],
|
|
128
|
+
blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
|
|
129
|
+
eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
|
|
130
|
+
}, {
|
|
131
|
+
name: 'handle_death_2',
|
|
132
|
+
params: [],
|
|
133
|
+
locals: [],
|
|
134
|
+
blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
|
|
135
|
+
eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
|
|
136
|
+
}],
|
|
137
|
+
};
|
|
138
|
+
const files = (0, mcfunction_1.generateDatapack)(mod);
|
|
139
|
+
const tickFn = files.find(f => f.path.includes('__tick.mcfunction'));
|
|
140
|
+
expect(tickFn).toBeDefined();
|
|
141
|
+
expect(tickFn.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death');
|
|
142
|
+
expect(tickFn.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death_2');
|
|
143
|
+
expect(tickFn.content).toContain('tag @a[tag=rs.just_died] remove rs.just_died');
|
|
144
|
+
});
|
|
120
145
|
});
|
|
121
146
|
//# sourceMappingURL=codegen.test.js.map
|
|
@@ -124,6 +124,184 @@ fn main() {
|
|
|
124
124
|
expect(mainFn).toContain('"objective":"rs"');
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
|
+
describe('timer builtins', () => {
|
|
128
|
+
it('generates scheduled timer helper functions', () => {
|
|
129
|
+
const files = compile(`
|
|
130
|
+
fn main() {
|
|
131
|
+
let intervalId: int = setInterval(20, () => {
|
|
132
|
+
say("tick");
|
|
133
|
+
});
|
|
134
|
+
setTimeout(100, () => {
|
|
135
|
+
say("later");
|
|
136
|
+
});
|
|
137
|
+
clearInterval(intervalId);
|
|
138
|
+
}
|
|
139
|
+
`);
|
|
140
|
+
const mainFn = getFunction(files, 'main');
|
|
141
|
+
const intervalFn = getFunction(files, '__interval_0');
|
|
142
|
+
const intervalBodyFn = getFunction(files, '__interval_body_0');
|
|
143
|
+
const timeoutFn = getFunction(files, '__timeout_0');
|
|
144
|
+
expect(mainFn).toBeDefined();
|
|
145
|
+
expect(mainFn).toContain('schedule function test:__interval_0 20t');
|
|
146
|
+
expect(mainFn).toContain('schedule function test:__timeout_0 100t');
|
|
147
|
+
expect(mainFn).toContain('schedule clear test:__interval_0');
|
|
148
|
+
expect(intervalFn).toContain('function test:__interval_body_0');
|
|
149
|
+
expect(intervalFn).toContain('schedule function test:__interval_0 20t');
|
|
150
|
+
expect(intervalBodyFn).toContain('say tick');
|
|
151
|
+
expect(timeoutFn).toContain('say later');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('is type narrowing', () => {
|
|
155
|
+
it('type checks and compiles entity narrowing inside foreach blocks', () => {
|
|
156
|
+
const source = `
|
|
157
|
+
fn main() {
|
|
158
|
+
foreach (e in @e) {
|
|
159
|
+
if (e is Player) {
|
|
160
|
+
kill(e);
|
|
161
|
+
}
|
|
162
|
+
if (e is Zombie) {
|
|
163
|
+
kill(e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
168
|
+
expect(typeCheck(source)).toEqual([]);
|
|
169
|
+
const files = compile(source);
|
|
170
|
+
const mainFn = getFunction(files, 'main');
|
|
171
|
+
const foreachFn = getSubFunction(files, 'main', 'foreach_0');
|
|
172
|
+
const thenFiles = files.filter(file => file.path.includes('/main/then_') && file.content.includes('kill @s'));
|
|
173
|
+
expect(mainFn).toContain('execute as @e run function test:main/foreach_0');
|
|
174
|
+
expect(foreachFn).toContain('execute if entity @s[type=player] run function test:main/then_');
|
|
175
|
+
expect(foreachFn).toContain('execute if entity @s[type=zombie] run function test:main/then_');
|
|
176
|
+
expect(thenFiles).toHaveLength(2);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe('impl blocks', () => {
|
|
180
|
+
it('compiles static and instance impl methods end to end', () => {
|
|
181
|
+
const source = `
|
|
182
|
+
struct Point { x: int, y: int }
|
|
183
|
+
|
|
184
|
+
impl Point {
|
|
185
|
+
fn new(x: int, y: int) -> Point {
|
|
186
|
+
return { x: x, y: y };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn distance(self) -> int {
|
|
190
|
+
return self.x + self.y;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn main() {
|
|
195
|
+
let p: Point = Point::new(1, 2);
|
|
196
|
+
let d: int = p.distance();
|
|
197
|
+
say("\${d}");
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
expect(typeCheck(source)).toEqual([]);
|
|
201
|
+
const files = compile(source);
|
|
202
|
+
const mainFn = getFunction(files, 'main');
|
|
203
|
+
const staticFn = getFunction(files, 'Point_new');
|
|
204
|
+
const instanceFn = getFunction(files, 'Point_distance');
|
|
205
|
+
expect(mainFn).toContain('function test:Point_new');
|
|
206
|
+
expect(mainFn).toContain('function test:Point_distance');
|
|
207
|
+
expect(staticFn).toBeDefined();
|
|
208
|
+
expect(instanceFn).toBeDefined();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe('namespace prefixing', () => {
|
|
212
|
+
it('prefixes user objectives but preserves mc_name and qualified objectives', () => {
|
|
213
|
+
const files = compile(`
|
|
214
|
+
fn main() {
|
|
215
|
+
scoreboard_set("timer", #rs, 100);
|
|
216
|
+
scoreboard_set(@s, "timer", 100);
|
|
217
|
+
scoreboard_set(@s, #health, 20);
|
|
218
|
+
scoreboard_set(@s, "custom.timer", 1);
|
|
219
|
+
}
|
|
220
|
+
`, 'pack');
|
|
221
|
+
const mainFn = getFunction(files, 'main');
|
|
222
|
+
expect(mainFn).toContain('scoreboard players set timer rs 100');
|
|
223
|
+
expect(mainFn).toContain('scoreboard players set @s pack.timer 100');
|
|
224
|
+
expect(mainFn).toContain('scoreboard players set @s health 20');
|
|
225
|
+
expect(mainFn).toContain('scoreboard players set @s custom.timer 1');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe('Timer OOP API', () => {
|
|
229
|
+
it('compiles the Timer impl API end to end', () => {
|
|
230
|
+
const source = `
|
|
231
|
+
struct Timer {
|
|
232
|
+
_id: int,
|
|
233
|
+
_duration: int
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
impl Timer {
|
|
237
|
+
fn new(duration: int) -> Timer {
|
|
238
|
+
scoreboard_set("timer_ticks", #rs, 0);
|
|
239
|
+
scoreboard_set("timer_active", #rs, 0);
|
|
240
|
+
return { _id: 0, _duration: duration };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn start(self) {
|
|
244
|
+
scoreboard_set("timer_active", #rs, 1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn pause(self) {
|
|
248
|
+
scoreboard_set("timer_active", #rs, 0);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn reset(self) {
|
|
252
|
+
scoreboard_set("timer_ticks", #rs, 0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn done(self) -> bool {
|
|
256
|
+
let ticks: int = scoreboard_get("timer_ticks", #rs);
|
|
257
|
+
return ticks >= self._duration;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fn tick(self) {
|
|
261
|
+
let active: int = scoreboard_get("timer_active", #rs);
|
|
262
|
+
let ticks: int = scoreboard_get("timer_ticks", #rs);
|
|
263
|
+
|
|
264
|
+
if (active == 1) {
|
|
265
|
+
if (ticks < self._duration) {
|
|
266
|
+
scoreboard_set("timer_ticks", #rs, ticks + 1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fn main() {
|
|
273
|
+
let t: Timer = Timer::new(100);
|
|
274
|
+
t.start();
|
|
275
|
+
t.tick();
|
|
276
|
+
let finished: bool = t.done();
|
|
277
|
+
if (finished) {
|
|
278
|
+
say("done");
|
|
279
|
+
}
|
|
280
|
+
t.pause();
|
|
281
|
+
t.reset();
|
|
282
|
+
}
|
|
283
|
+
`;
|
|
284
|
+
expect(typeCheck(source)).toEqual([]);
|
|
285
|
+
const files = compile(source, 'timerapi');
|
|
286
|
+
const mainFn = getFunction(files, 'main');
|
|
287
|
+
const newFn = getFunction(files, 'Timer_new');
|
|
288
|
+
const startFn = getFunction(files, 'Timer_start');
|
|
289
|
+
const tickFn = getFunction(files, 'Timer_tick');
|
|
290
|
+
const doneFn = getFunction(files, 'Timer_done');
|
|
291
|
+
const pauseFn = getFunction(files, 'Timer_pause');
|
|
292
|
+
const resetFn = getFunction(files, 'Timer_reset');
|
|
293
|
+
expect(mainFn).toContain('function timerapi:Timer_new');
|
|
294
|
+
expect(mainFn).toContain('function timerapi:Timer_start');
|
|
295
|
+
expect(mainFn).toContain('function timerapi:Timer_tick');
|
|
296
|
+
expect(mainFn).toContain('function timerapi:Timer_done');
|
|
297
|
+
expect(newFn).toContain('scoreboard players set timer_ticks rs 0');
|
|
298
|
+
expect(startFn).toContain('scoreboard players set timer_active rs 1');
|
|
299
|
+
expect(tickFn).toContain('scoreboard players get timer_active rs');
|
|
300
|
+
expect(doneFn).toContain('scoreboard players get timer_ticks rs');
|
|
301
|
+
expect(pauseFn).toContain('scoreboard players set timer_active rs 0');
|
|
302
|
+
expect(resetFn).toContain('scoreboard players set timer_ticks rs 0');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
127
305
|
describe('advancement event decorators', () => {
|
|
128
306
|
it('generates advancement json with reward function path', () => {
|
|
129
307
|
const source = `
|
|
@@ -276,12 +454,12 @@ fn test() {
|
|
|
276
454
|
}
|
|
277
455
|
`;
|
|
278
456
|
const fn = getFunction(compile(source), 'test');
|
|
279
|
-
expect(fn).toContain('scoreboard objectives setdisplay sidebar kills');
|
|
280
|
-
expect(fn).toContain('scoreboard objectives setdisplay list coins');
|
|
281
|
-
expect(fn).toContain('scoreboard objectives setdisplay belowName hp');
|
|
457
|
+
expect(fn).toContain('scoreboard objectives setdisplay sidebar test.kills');
|
|
458
|
+
expect(fn).toContain('scoreboard objectives setdisplay list test.coins');
|
|
459
|
+
expect(fn).toContain('scoreboard objectives setdisplay belowName test.hp');
|
|
282
460
|
expect(fn).toContain('scoreboard objectives setdisplay sidebar');
|
|
283
|
-
expect(fn).toContain('scoreboard objectives add kills playerKillCount "Kill Count"');
|
|
284
|
-
expect(fn).toContain('scoreboard objectives remove kills');
|
|
461
|
+
expect(fn).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"');
|
|
462
|
+
expect(fn).toContain('scoreboard objectives remove test.kills');
|
|
285
463
|
});
|
|
286
464
|
it('compiles bossbar builtins', () => {
|
|
287
465
|
const source = `
|
|
@@ -485,7 +663,7 @@ fn test() -> int {
|
|
|
485
663
|
const fn = getFunction(files, 'test');
|
|
486
664
|
expect(fn).toBeDefined();
|
|
487
665
|
expect(fn).toContain('execute store result score');
|
|
488
|
-
expect(fn).toContain('scoreboard players get PlayerName kill_count');
|
|
666
|
+
expect(fn).toContain('scoreboard players get PlayerName test.kill_count');
|
|
489
667
|
});
|
|
490
668
|
it('compiles scoreboard_get with @s selector', () => {
|
|
491
669
|
const source = `
|
|
@@ -497,7 +675,7 @@ fn test() -> int {
|
|
|
497
675
|
const files = compile(source);
|
|
498
676
|
const fn = getFunction(files, 'test');
|
|
499
677
|
expect(fn).toBeDefined();
|
|
500
|
-
expect(fn).toContain('scoreboard players get @s kill_count');
|
|
678
|
+
expect(fn).toContain('scoreboard players get @s test.kill_count');
|
|
501
679
|
});
|
|
502
680
|
it('compiles scoreboard_set with constant value', () => {
|
|
503
681
|
const source = `
|
|
@@ -508,7 +686,7 @@ fn test() {
|
|
|
508
686
|
const files = compile(source);
|
|
509
687
|
const fn = getFunction(files, 'test');
|
|
510
688
|
expect(fn).toBeDefined();
|
|
511
|
-
expect(fn).toContain('scoreboard players set PlayerName kill_count 100');
|
|
689
|
+
expect(fn).toContain('scoreboard players set PlayerName test.kill_count 100');
|
|
512
690
|
});
|
|
513
691
|
it('compiles scoreboard_set with variable value', () => {
|
|
514
692
|
const source = `
|
|
@@ -522,7 +700,7 @@ fn test() {
|
|
|
522
700
|
.filter(f => f.path.includes('test'))
|
|
523
701
|
.map(f => f.content)
|
|
524
702
|
.join('\n');
|
|
525
|
-
expect(allContent).toContain('execute store result score @s score');
|
|
703
|
+
expect(allContent).toContain('execute store result score @s test.score');
|
|
526
704
|
});
|
|
527
705
|
it('compiles score() as expression', () => {
|
|
528
706
|
const source = `
|
|
@@ -548,7 +726,7 @@ fn double_score() -> int {
|
|
|
548
726
|
const files = compile(source);
|
|
549
727
|
const fn = getFunction(files, 'double_score');
|
|
550
728
|
expect(fn).toBeDefined();
|
|
551
|
-
expect(fn).toContain('scoreboard players get @s points');
|
|
729
|
+
expect(fn).toContain('scoreboard players get @s test.points');
|
|
552
730
|
});
|
|
553
731
|
});
|
|
554
732
|
describe('Built-in functions', () => {
|
|
@@ -1342,10 +1520,10 @@ fn heal(amount: int) {
|
|
|
1342
1520
|
});
|
|
1343
1521
|
describe('backward compat: string objective still works', () => {
|
|
1344
1522
|
const source = `fn test() { let x: int = scoreboard_get(@s, "kills"); }`;
|
|
1345
|
-
it('
|
|
1523
|
+
it('prefixes plain string objectives with the active namespace', () => {
|
|
1346
1524
|
const files = compile(source, 'compat');
|
|
1347
1525
|
const fn = getFunction(files, 'test');
|
|
1348
|
-
expect(fn).toContain('scoreboard players get @s kills');
|
|
1526
|
+
expect(fn).toContain('scoreboard players get @s compat.kills');
|
|
1349
1527
|
});
|
|
1350
1528
|
});
|
|
1351
1529
|
describe('#mc_name with fake player target', () => {
|
|
@@ -10,10 +10,16 @@ function kinds(tokens) {
|
|
|
10
10
|
describe('Lexer', () => {
|
|
11
11
|
describe('keywords', () => {
|
|
12
12
|
it('recognizes all keywords', () => {
|
|
13
|
-
const tokens = tokenize('fn let const if else while for foreach match return as at in struct enum trigger namespace');
|
|
13
|
+
const tokens = tokenize('fn let const if else while for foreach match return as at in is struct impl enum trigger namespace');
|
|
14
14
|
expect(kinds(tokens)).toEqual([
|
|
15
15
|
'fn', 'let', 'const', 'if', 'else', 'while', 'for', 'foreach', 'match',
|
|
16
|
-
'return', 'as', 'at', 'in', 'struct', 'enum', 'trigger', 'namespace', 'eof'
|
|
16
|
+
'return', 'as', 'at', 'in', 'is', 'struct', 'impl', 'enum', 'trigger', 'namespace', 'eof'
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
it('tokenizes is-check and impl syntax with their dedicated keywords', () => {
|
|
20
|
+
const tokens = tokenize('if (e is Player) { } impl Point { }');
|
|
21
|
+
expect(kinds(tokens)).toEqual([
|
|
22
|
+
'if', '(', 'ident', 'is', 'ident', ')', '{', '}', 'impl', 'ident', '{', '}', 'eof',
|
|
17
23
|
]);
|
|
18
24
|
});
|
|
19
25
|
it('recognizes type keywords', () => {
|
|
@@ -169,6 +175,10 @@ describe('Lexer', () => {
|
|
|
169
175
|
const tokens = tokenize('=>');
|
|
170
176
|
expect(kinds(tokens)).toEqual(['=>', 'eof']);
|
|
171
177
|
});
|
|
178
|
+
it('tokenizes static method separators for impl methods', () => {
|
|
179
|
+
const tokens = tokenize('Point::new()');
|
|
180
|
+
expect(kinds(tokens)).toEqual(['ident', '::', 'ident', '(', ')', 'eof']);
|
|
181
|
+
});
|
|
172
182
|
});
|
|
173
183
|
describe('delimiters', () => {
|
|
174
184
|
it('tokenizes all delimiters', () => {
|