redscript-mc 2.1.0 → 2.1.1

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/README.md CHANGED
@@ -192,6 +192,22 @@ fn on_diamond() {
192
192
  fn on_death() {
193
193
  scoreboard_add(@s, #deaths, 1);
194
194
  }
195
+
196
+ // Delay execution by N ticks (20t = 1 second)
197
+ @schedule(ticks=20)
198
+ fn after_one_second(): void {
199
+ title(@a, "One second later!");
200
+ }
201
+
202
+ // Spread a heavy loop across multiple ticks (batch=N iterations/tick)
203
+ @coroutine(batch=50, onDone=all_done)
204
+ fn process_all(): void {
205
+ let i: int = 0;
206
+ while (i < 1000) {
207
+ // work spread over ~20 ticks instead of lagging one tick
208
+ i = i + 1;
209
+ }
210
+ }
195
211
  ```
196
212
 
197
213
  #### Control Flow
@@ -316,6 +332,26 @@ fn show_fib() {
316
332
 
317
333
  ---
318
334
 
335
+ ### Examples
336
+
337
+ The `examples/` directory contains ready-to-compile demos:
338
+
339
+ | File | What it shows |
340
+ |---|---|
341
+ | `readme-demo.mcrs` | Real-time sine wave particles — `@tick`, `foreach`, f-strings, math stdlib |
342
+ | `math-showcase.mcrs` | All stdlib math modules: trig, vectors, BigInt, fractals |
343
+ | `showcase.mcrs` | Full feature tour: structs, enums, `match`, lambdas, `@tick`/`@load` |
344
+ | `coroutine-demo.mcrs` | `@coroutine(batch=50)` — spread 1000 iterations across ~20 ticks |
345
+ | `enum-demo.mcrs` | Enum state machine: NPC AI cycling Idle → Moving → Attacking with `match` |
346
+ | `scheduler-demo.mcrs` | `@schedule(ticks=20)` — delayed events, chained schedules |
347
+
348
+ Compile any example:
349
+ ```bash
350
+ node dist/cli.js compile examples/coroutine-demo.mcrs -o ~/mc-server/datapacks/demo --namespace demo
351
+ ```
352
+
353
+ ---
354
+
319
355
  ### Further Reading
320
356
 
321
357
  | | |
@@ -10,6 +10,7 @@ const lexer_1 = require("../lexer");
10
10
  const parser_1 = require("../parser");
11
11
  const typechecker_1 = require("../typechecker");
12
12
  const diagnostics_1 = require("../diagnostics");
13
+ const metadata_1 = require("../builtins/metadata");
13
14
  // ---------------------------------------------------------------------------
14
15
  // Helpers mirrored from lsp/server.ts (tested independently)
15
16
  // ---------------------------------------------------------------------------
@@ -231,6 +232,81 @@ fn anotherFn(x: int): int { return x; }
231
232
  });
232
233
  });
233
234
  // ---------------------------------------------------------------------------
235
+ // Hover — builtin functions
236
+ // ---------------------------------------------------------------------------
237
+ const DECORATOR_DOCS = {
238
+ tick: 'Runs every MC game tick (~20 Hz). No arguments.',
239
+ load: 'Runs on `/reload`. Use for initialization logic.',
240
+ coroutine: 'Splits loops into tick-spread continuations. Arg: `batch=N` (steps per tick, default 1).',
241
+ schedule: 'Schedules the function to run after N ticks. Arg: `ticks=N`.',
242
+ on_trigger: 'Runs when a trigger scoreboard objective is set by a player. Arg: trigger name.',
243
+ keep: 'Prevents the compiler from dead-code-eliminating this function.',
244
+ on: 'Generic event handler decorator.',
245
+ on_advancement: 'Runs when a player earns an advancement. Arg: advancement id.',
246
+ on_craft: 'Runs when a player crafts an item. Arg: item id.',
247
+ on_death: 'Runs when a player dies.',
248
+ on_join_team: 'Runs when a player joins a team. Arg: team name.',
249
+ on_login: 'Runs when a player logs in.',
250
+ };
251
+ describe('LSP hover — builtin functions', () => {
252
+ it('has metadata for say', () => {
253
+ const b = metadata_1.BUILTIN_METADATA['say'];
254
+ expect(b).toBeDefined();
255
+ expect(b.params.length).toBeGreaterThan(0);
256
+ expect(b.returns).toBe('void');
257
+ expect(b.doc).toBeTruthy();
258
+ });
259
+ it('has metadata for kill', () => {
260
+ const b = metadata_1.BUILTIN_METADATA['kill'];
261
+ expect(b).toBeDefined();
262
+ expect(b.returns).toBe('void');
263
+ });
264
+ it('has metadata for tellraw', () => {
265
+ const b = metadata_1.BUILTIN_METADATA['tellraw'];
266
+ expect(b).toBeDefined();
267
+ const paramStr = b.params.map(p => `${p.name}: ${p.type}${p.required ? '' : '?'}`).join(', ');
268
+ const sig = `fn ${b.name}(${paramStr}): ${b.returns}`;
269
+ expect(sig).toMatch(/^fn tellraw/);
270
+ expect(sig).toContain('target');
271
+ });
272
+ it('formats builtin hover markdown', () => {
273
+ const b = metadata_1.BUILTIN_METADATA['particle'];
274
+ expect(b).toBeDefined();
275
+ const paramStr = b.params.map(p => `${p.name}: ${p.type}${p.required ? '' : '?'}`).join(', ');
276
+ const sig = `fn ${b.name}(${paramStr}): ${b.returns}`;
277
+ const markdown = `\`\`\`redscript\n${sig}\n\`\`\`\n${b.doc}`;
278
+ expect(markdown).toContain('```redscript');
279
+ expect(markdown).toContain('fn particle');
280
+ expect(markdown).toContain(b.doc);
281
+ });
282
+ });
283
+ // ---------------------------------------------------------------------------
284
+ // Hover — decorators
285
+ // ---------------------------------------------------------------------------
286
+ describe('LSP hover — decorators', () => {
287
+ it('has docs for @tick', () => {
288
+ expect(DECORATOR_DOCS['tick']).toContain('tick');
289
+ });
290
+ it('has docs for @load', () => {
291
+ expect(DECORATOR_DOCS['load']).toContain('reload');
292
+ });
293
+ it('has docs for @coroutine', () => {
294
+ expect(DECORATOR_DOCS['coroutine']).toContain('batch');
295
+ });
296
+ it('has docs for @schedule', () => {
297
+ expect(DECORATOR_DOCS['schedule']).toContain('ticks');
298
+ });
299
+ it('has docs for @on_trigger', () => {
300
+ expect(DECORATOR_DOCS['on_trigger']).toContain('trigger');
301
+ });
302
+ it('formats decorator hover markdown', () => {
303
+ const name = 'tick';
304
+ const doc = DECORATOR_DOCS[name];
305
+ const markdown = `**@${name}** — ${doc}`;
306
+ expect(markdown).toBe(`**@tick** — ${DECORATOR_DOCS['tick']}`);
307
+ });
308
+ });
309
+ // ---------------------------------------------------------------------------
234
310
  // Server module import (smoke test — does not start stdio)
235
311
  // ---------------------------------------------------------------------------
236
312
  describe('LSP server module', () => {
@@ -38,7 +38,7 @@ const path = __importStar(require("path"));
38
38
  const compile_1 = require("../compile");
39
39
  const mc_validator_1 = require("../mc-validator");
40
40
  const FIXTURE_PATH = path.join(__dirname, 'fixtures', 'mc-commands-1.21.4.json');
41
- const EXAMPLES = ['counter', 'arena', 'shop', 'quiz', 'turret'];
41
+ const EXAMPLES = ['shop', 'quiz', 'turret'];
42
42
  function getCommands(source, namespace = 'test') {
43
43
  const result = (0, compile_1.compile)(source, { namespace });
44
44
  expect(result.success).toBe(true);
@@ -56,11 +56,6 @@ function validateSource(validator, source, namespace) {
56
56
  }
57
57
  describe('MC Command Syntax Validation', () => {
58
58
  const validator = new mc_validator_1.MCCommandValidator(FIXTURE_PATH);
59
- test('counter example generates valid MC commands', () => {
60
- const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.mcrs'), 'utf-8');
61
- const errors = validateSource(validator, src, 'counter');
62
- expect(errors).toHaveLength(0);
63
- });
64
59
  EXAMPLES.forEach(name => {
65
60
  test(`${name}.mcrs generates valid MC commands`, () => {
66
61
  const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.mcrs`), 'utf-8');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const path = __importStar(require("path"));
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const compile_1 = require("../emit/compile");
40
+ describe('stdlib include path', () => {
41
+ it('import "stdlib/math" resolves to the stdlib math module', () => {
42
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-stdlib-'));
43
+ const mainPath = path.join(tempDir, 'main.mcrs');
44
+ fs.writeFileSync(mainPath, 'import "stdlib/math";\nfn main() { let x: int = abs(-5); }\n');
45
+ const source = fs.readFileSync(mainPath, 'utf-8');
46
+ const result = (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath });
47
+ expect(result.files.some(f => f.path.includes('abs'))).toBe(true);
48
+ });
49
+ it('import "stdlib/math.mcrs" also resolves (explicit extension)', () => {
50
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-stdlib-'));
51
+ const mainPath = path.join(tempDir, 'main.mcrs');
52
+ fs.writeFileSync(mainPath, 'import "stdlib/math.mcrs";\nfn main() { let x: int = abs(-5); }\n');
53
+ const source = fs.readFileSync(mainPath, 'utf-8');
54
+ const result = (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath });
55
+ expect(result.files.some(f => f.path.includes('abs'))).toBe(true);
56
+ });
57
+ it('import "stdlib/vec" resolves to the stdlib vec module', () => {
58
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-stdlib-'));
59
+ const mainPath = path.join(tempDir, 'main.mcrs');
60
+ fs.writeFileSync(mainPath, 'import "stdlib/vec";\nfn main() { let d: int = dot2d(1, 2, 3, 4); }\n');
61
+ const source = fs.readFileSync(mainPath, 'utf-8');
62
+ const result = (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath });
63
+ expect(result.files.some(f => f.path.includes('dot2d'))).toBe(true);
64
+ });
65
+ it('non-existent stdlib module gives a clear error', () => {
66
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-stdlib-'));
67
+ const mainPath = path.join(tempDir, 'main.mcrs');
68
+ fs.writeFileSync(mainPath, 'import "stdlib/nonexistent";\nfn main() {}\n');
69
+ const source = fs.readFileSync(mainPath, 'utf-8');
70
+ expect(() => (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath }))
71
+ .toThrow(/Cannot import/);
72
+ });
73
+ it('--include flag allows importing from custom directory', () => {
74
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rs-include-'));
75
+ const libDir = path.join(tempDir, 'mylibs');
76
+ fs.mkdirSync(libDir);
77
+ const mainPath = path.join(tempDir, 'main.mcrs');
78
+ const libPath = path.join(libDir, 'helpers.mcrs');
79
+ fs.writeFileSync(libPath, 'fn triple(x: int) -> int { return x + x + x; }\n');
80
+ fs.writeFileSync(mainPath, 'import "helpers";\nfn main() { let x: int = triple(3); }\n');
81
+ const source = fs.readFileSync(mainPath, 'utf-8');
82
+ const result = (0, compile_1.compile)(source, { namespace: 'test', filePath: mainPath, includeDirs: [libDir] });
83
+ expect(result.files.some(f => f.path.includes('triple'))).toBe(true);
84
+ });
85
+ });
86
+ //# sourceMappingURL=stdlib-include.test.js.map
package/dist/src/cli.js CHANGED
@@ -89,6 +89,7 @@ Options:
89
89
  --mc-version <ver> Target Minecraft version (default: 1.21). Affects codegen features.
90
90
  e.g. --mc-version 1.20.2, --mc-version 1.19
91
91
  --lenient Treat type errors as warnings instead of blocking compilation
92
+ --include <dir> Add a directory to the import search path (repeatable)
92
93
  -h, --help Show this help message
93
94
  `);
94
95
  }
@@ -210,6 +211,12 @@ function parseArgs(args) {
210
211
  result.lenient = true;
211
212
  i++;
212
213
  }
214
+ else if (arg === '--include') {
215
+ if (!result.includeDirs)
216
+ result.includeDirs = [];
217
+ result.includeDirs.push(args[++i]);
218
+ i++;
219
+ }
213
220
  else if (!result.command) {
214
221
  result.command = arg;
215
222
  i++;
@@ -229,7 +236,7 @@ function deriveNamespace(filePath) {
229
236
  // Convert to valid identifier: lowercase, replace non-alphanumeric with underscore
230
237
  return basename.toLowerCase().replace(/[^a-z0-9]/g, '_');
231
238
  }
232
- function compileCommand(file, output, namespace, sourceMap = false, mcVersionStr, lenient = false) {
239
+ function compileCommand(file, output, namespace, sourceMap = false, mcVersionStr, lenient = false, includeDirs) {
233
240
  // Read source file
234
241
  if (!fs.existsSync(file)) {
235
242
  console.error(`Error: File not found: ${file}`);
@@ -247,7 +254,7 @@ function compileCommand(file, output, namespace, sourceMap = false, mcVersionStr
247
254
  }
248
255
  const source = fs.readFileSync(file, 'utf-8');
249
256
  try {
250
- const result = (0, index_1.compile)(source, { namespace, filePath: file, generateSourceMap: sourceMap, mcVersion, lenient });
257
+ const result = (0, index_1.compile)(source, { namespace, filePath: file, generateSourceMap: sourceMap, mcVersion, lenient, includeDirs });
251
258
  for (const w of result.warnings) {
252
259
  console.error(`Warning: ${w}`);
253
260
  }
@@ -411,7 +418,7 @@ async function main() {
411
418
  {
412
419
  const namespace = parsed.namespace ?? deriveNamespace(parsed.file);
413
420
  const output = parsed.output ?? './dist';
414
- compileCommand(parsed.file, output, namespace, parsed.sourceMap, parsed.mcVersionStr, parsed.lenient);
421
+ compileCommand(parsed.file, output, namespace, parsed.sourceMap, parsed.mcVersionStr, parsed.lenient, parsed.includeDirs);
415
422
  }
416
423
  break;
417
424
  case 'watch':
@@ -32,6 +32,7 @@ export declare function resolveSourceLine(combinedLine: number, ranges: SourceRa
32
32
  interface PreprocessOptions {
33
33
  filePath?: string;
34
34
  seen?: Set<string>;
35
+ includeDirs?: string[];
35
36
  }
36
37
  export declare function preprocessSourceWithMetadata(source: string, options?: PreprocessOptions): PreprocessedSource;
37
38
  export declare function preprocessSource(source: string, options?: PreprocessOptions): string;
@@ -75,6 +75,31 @@ function isLibrarySource(source) {
75
75
  }
76
76
  return false;
77
77
  }
78
+ /** Resolve an import specifier to an absolute file path, trying multiple locations. */
79
+ function resolveImportPath(spec, fromFile, includeDirs) {
80
+ const candidates = spec.endsWith('.mcrs') ? [spec] : [spec, spec + '.mcrs'];
81
+ for (const candidate of candidates) {
82
+ // 1. Relative to the importing file
83
+ const rel = path.resolve(path.dirname(fromFile), candidate);
84
+ if (fs.existsSync(rel))
85
+ return rel;
86
+ // 2. stdlib directory (package root / src / stdlib)
87
+ // Strip leading 'stdlib/' prefix so `import "stdlib/math"` resolves to
88
+ // <stdlibDir>/math.mcrs rather than <stdlibDir>/stdlib/math.mcrs.
89
+ const stdlibDir = path.resolve(__dirname, '..', 'src', 'stdlib');
90
+ const stdlibCandidate = candidate.replace(/^stdlib\//, '');
91
+ const stdlib = path.resolve(stdlibDir, stdlibCandidate);
92
+ if (fs.existsSync(stdlib))
93
+ return stdlib;
94
+ // 3. Extra include dirs
95
+ for (const dir of includeDirs) {
96
+ const extra = path.resolve(dir, candidate);
97
+ if (fs.existsSync(extra))
98
+ return extra;
99
+ }
100
+ }
101
+ return null;
102
+ }
78
103
  function countLines(source) {
79
104
  return source === '' ? 0 : source.split('\n').length;
80
105
  }
@@ -88,6 +113,7 @@ function offsetRanges(ranges, lineOffset) {
88
113
  function preprocessSourceWithMetadata(source, options = {}) {
89
114
  const { filePath } = options;
90
115
  const seen = options.seen ?? new Set();
116
+ const includeDirs = options.includeDirs ?? [];
91
117
  if (filePath) {
92
118
  seen.add(path.resolve(filePath));
93
119
  }
@@ -105,27 +131,24 @@ function preprocessSourceWithMetadata(source, options = {}) {
105
131
  if (!filePath) {
106
132
  throw new diagnostics_1.DiagnosticError('ParseError', 'Import statements require a file path', { line: i + 1, col: 1 }, lines);
107
133
  }
108
- const importPath = path.resolve(path.dirname(filePath), match[1]);
134
+ const importPath = resolveImportPath(match[1], filePath, includeDirs);
135
+ if (!importPath) {
136
+ throw new diagnostics_1.DiagnosticError('ParseError', `Cannot import '${match[1]}'`, { file: filePath, line: i + 1, col: 1 }, lines);
137
+ }
109
138
  if (!seen.has(importPath)) {
110
139
  seen.add(importPath);
111
- let importedSource;
112
- try {
113
- importedSource = fs.readFileSync(importPath, 'utf-8');
114
- }
115
- catch {
116
- throw new diagnostics_1.DiagnosticError('ParseError', `Cannot import '${match[1]}'`, { file: filePath, line: i + 1, col: 1 }, lines);
117
- }
140
+ const importedSource = fs.readFileSync(importPath, 'utf-8');
118
141
  if (isLibrarySource(importedSource)) {
119
142
  // Library file: parse separately so its functions are DCE-eligible.
120
143
  // Also collect any transitive library imports inside it.
121
- const nested = preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen });
144
+ const nested = preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen, includeDirs });
122
145
  libraryImports.push({ source: importedSource, filePath: importPath });
123
146
  // Propagate transitive library imports (e.g. math.mcrs imports vec.mcrs)
124
147
  if (nested.libraryImports)
125
148
  libraryImports.push(...nested.libraryImports);
126
149
  }
127
150
  else {
128
- imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }));
151
+ imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen, includeDirs }));
129
152
  }
130
153
  }
131
154
  continue;
@@ -19,6 +19,8 @@ export interface CompileOptions {
19
19
  * Use for gradual migration or testing with existing codebases that have type errors.
20
20
  */
21
21
  lenient?: boolean;
22
+ /** Extra directories to search when resolving imports (in addition to relative and stdlib). */
23
+ includeDirs?: string[];
22
24
  }
23
25
  export interface CompileResult {
24
26
  files: DatapackFile[];
@@ -22,10 +22,10 @@ const budget_1 = require("../lir/budget");
22
22
  const mc_version_1 = require("../types/mc-version");
23
23
  const typechecker_1 = require("../typechecker");
24
24
  function compile(source, options = {}) {
25
- const { namespace = 'redscript', filePath, generateSourceMap = false, mcVersion = mc_version_1.DEFAULT_MC_VERSION, lenient = false } = options;
25
+ const { namespace = 'redscript', filePath, generateSourceMap = false, mcVersion = mc_version_1.DEFAULT_MC_VERSION, lenient = false, includeDirs } = options;
26
26
  const warnings = [];
27
27
  // Preprocess: resolve import directives, merge imported sources
28
- const preprocessed = (0, compile_1.preprocessSourceWithMetadata)(source, { filePath });
28
+ const preprocessed = (0, compile_1.preprocessSourceWithMetadata)(source, { filePath, includeDirs });
29
29
  const processedSource = preprocessed.source;
30
30
  // Stage 1: Lex + Parse → AST
31
31
  const lexer = new lexer_1.Lexer(processedSource);
@@ -16,6 +16,7 @@ const lexer_1 = require("../lexer");
16
16
  const parser_1 = require("../parser");
17
17
  const typechecker_1 = require("../typechecker");
18
18
  const diagnostics_1 = require("../diagnostics");
19
+ const metadata_1 = require("../builtins/metadata");
19
20
  // ---------------------------------------------------------------------------
20
21
  // Connection and document manager
21
22
  // ---------------------------------------------------------------------------
@@ -82,6 +83,23 @@ function toDiagnostic(err) {
82
83
  };
83
84
  }
84
85
  // ---------------------------------------------------------------------------
86
+ // Decorator hover docs
87
+ // ---------------------------------------------------------------------------
88
+ const DECORATOR_DOCS = {
89
+ tick: 'Runs every MC game tick (~20 Hz). No arguments.',
90
+ load: 'Runs on `/reload`. Use for initialization logic.',
91
+ coroutine: 'Splits loops into tick-spread continuations. Arg: `batch=N` (steps per tick, default 1).',
92
+ schedule: 'Schedules the function to run after N ticks. Arg: `ticks=N`.',
93
+ on_trigger: 'Runs when a trigger scoreboard objective is set by a player. Arg: trigger name.',
94
+ keep: 'Prevents the compiler from dead-code-eliminating this function.',
95
+ on: 'Generic event handler decorator.',
96
+ on_advancement: 'Runs when a player earns an advancement. Arg: advancement id.',
97
+ on_craft: 'Runs when a player crafts an item. Arg: item id.',
98
+ on_death: 'Runs when a player dies.',
99
+ on_join_team: 'Runs when a player joins a team. Arg: team name.',
100
+ on_login: 'Runs when a player logs in.',
101
+ };
102
+ // ---------------------------------------------------------------------------
85
103
  // Hover helpers
86
104
  // ---------------------------------------------------------------------------
87
105
  /** Find the word at a position in a text. */
@@ -239,9 +257,42 @@ connection.onHover((params) => {
239
257
  const program = cached?.program ?? null;
240
258
  if (!program)
241
259
  return null;
260
+ // Check if cursor is on a decorator (@tick, @load, etc.)
261
+ const lines = source.split('\n');
262
+ const lineText = lines[params.position.line] ?? '';
263
+ const decoratorMatch = lineText.match(/@([a-zA-Z_][a-zA-Z0-9_]*)/);
264
+ if (decoratorMatch) {
265
+ const ch = params.position.character;
266
+ const atIdx = lineText.indexOf('@');
267
+ const decoratorEnd = atIdx + 1 + decoratorMatch[1].length;
268
+ if (ch >= atIdx && ch <= decoratorEnd) {
269
+ const decoratorName = decoratorMatch[1];
270
+ const decoratorDoc = DECORATOR_DOCS[decoratorName];
271
+ if (decoratorDoc) {
272
+ const content = {
273
+ kind: node_1.MarkupKind.Markdown,
274
+ value: `**@${decoratorName}** — ${decoratorDoc}`,
275
+ };
276
+ return { contents: content };
277
+ }
278
+ }
279
+ }
242
280
  const word = wordAt(source, params.position);
243
281
  if (!word)
244
282
  return null;
283
+ // Check builtins
284
+ const builtin = metadata_1.BUILTIN_METADATA[word];
285
+ if (builtin) {
286
+ const paramStr = builtin.params
287
+ .map(p => `${p.name}: ${p.type}${p.required ? '' : '?'}`)
288
+ .join(', ');
289
+ const sig = `fn ${builtin.name}(${paramStr}): ${builtin.returns}`;
290
+ const content = {
291
+ kind: node_1.MarkupKind.Markdown,
292
+ value: `\`\`\`redscript\n${sig}\n\`\`\`\n${builtin.doc}`,
293
+ };
294
+ return { contents: content };
295
+ }
245
296
  // Check if it's a known function
246
297
  const fn = findFunction(program, word);
247
298
  if (fn) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "redscript-vscode",
9
- "version": "1.2.8",
9
+ "version": "1.2.9",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "redscript": "file:../../",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "../..": {
26
26
  "name": "redscript-mc",
27
- "version": "2.1.0",
27
+ "version": "2.1.1",
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
30
  "vscode-languageserver": "^9.0.1",
@@ -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.2.8",
5
+ "version": "1.2.9",
6
6
  "publisher": "bkmashiro",
7
7
  "icon": "icon.png",
8
8
  "license": "MIT",
@@ -8,6 +8,7 @@
8
8
  "patterns": [
9
9
  { "include": "#comments" },
10
10
  { "include": "#decorators" },
11
+ { "include": "#fstring" },
11
12
  { "include": "#string-interpolation" },
12
13
  { "include": "#strings" },
13
14
  { "include": "#keywords" },
@@ -91,6 +92,39 @@
91
92
  ]
92
93
  },
93
94
 
95
+ "fstring": {
96
+ "comment": "f-string: f\"Hello {name}\" — f prefix gets special color, {expr} highlighted",
97
+ "begin": "(f)(\")",
98
+ "beginCaptures": {
99
+ "1": { "name": "storage.type.string.redscript" },
100
+ "2": { "name": "punctuation.definition.string.begin.redscript" }
101
+ },
102
+ "end": "\"",
103
+ "endCaptures": {
104
+ "0": { "name": "punctuation.definition.string.end.redscript" }
105
+ },
106
+ "name": "string.interpolated.redscript",
107
+ "patterns": [
108
+ {
109
+ "begin": "\\{",
110
+ "end": "\\}",
111
+ "beginCaptures": { "0": { "name": "punctuation.definition.interpolation.begin.redscript" } },
112
+ "endCaptures": { "0": { "name": "punctuation.definition.interpolation.end.redscript" } },
113
+ "contentName": "meta.interpolation.redscript",
114
+ "patterns": [
115
+ { "include": "#selectors" },
116
+ { "include": "#numbers" },
117
+ { "include": "#fn-call" },
118
+ {
119
+ "name": "variable.other.redscript",
120
+ "match": "[a-zA-Z_][a-zA-Z0-9_]*"
121
+ }
122
+ ]
123
+ },
124
+ { "name": "constant.character.escape.redscript", "match": "\\\\." }
125
+ ]
126
+ },
127
+
94
128
  "string-interpolation": {
95
129
  "comment": "Interpolated string with ${expr} — highlight the expression inside",
96
130
  "begin": "\"(?=[^\"]*\\$\\{)",
@@ -0,0 +1,50 @@
1
+ // coroutine-demo.mcrs — Spread particle generation across multiple ticks
2
+ //
3
+ // @coroutine(batch=50) processes 50 iterations per tick, so 1000 total
4
+ // iterations take ~20 ticks instead of lagging one tick.
5
+ //
6
+ // Usage:
7
+ // /function demo:start_particle_wave begin the wave
8
+ // /function demo:stop_particle_wave cancel the wave
9
+
10
+ import "../src/stdlib/math.mcrs"
11
+
12
+ let wave_running: bool = false;
13
+
14
+ @load
15
+ fn init() {
16
+ wave_running = false;
17
+ }
18
+
19
+ // Spread 1000 iterations across ~20 ticks (50 per tick)
20
+ @coroutine(batch=50, onDone=wave_done)
21
+ fn generate_wave(): void {
22
+ let i: int = 0;
23
+ while (i < 1000) {
24
+ let angle: int = (i * 360) / 1000;
25
+ let px: int = sin_fixed(angle);
26
+ let py: int = cos_fixed(angle);
27
+ raw("particle minecraft:end_rod ^${px} ^100 ^${py} 0 0 0 0 1 force @a");
28
+ i = i + 1;
29
+ }
30
+ }
31
+
32
+ fn wave_done(): void {
33
+ wave_running = false;
34
+ title(@a, "Wave complete!");
35
+ }
36
+
37
+ fn start_particle_wave(): void {
38
+ if (wave_running) {
39
+ tell(@a, "Wave already running.");
40
+ return;
41
+ }
42
+ wave_running = true;
43
+ title(@a, "Starting particle wave...");
44
+ generate_wave();
45
+ }
46
+
47
+ fn stop_particle_wave(): void {
48
+ wave_running = false;
49
+ title(@a, "Wave stopped.");
50
+ }