redscript-mc 1.2.19 → 1.2.21

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.
@@ -2,7 +2,8 @@
2
2
  * Compile-all smoke test
3
3
  *
4
4
  * Finds every .mcrs file in the repo (excluding declaration files and node_modules)
5
- * and verifies that each one compiles without throwing an error.
5
+ * and verifies that each one compiles without errors via the CLI (which handles
6
+ * `import` statements, unlike the bare `compile()` function).
6
7
  *
7
8
  * This catches regressions where a language change breaks existing source files.
8
9
  */
@@ -3,7 +3,8 @@
3
3
  * Compile-all smoke test
4
4
  *
5
5
  * Finds every .mcrs file in the repo (excluding declaration files and node_modules)
6
- * and verifies that each one compiles without throwing an error.
6
+ * and verifies that each one compiles without errors via the CLI (which handles
7
+ * `import` statements, unlike the bare `compile()` function).
7
8
  *
8
9
  * This catches regressions where a language change breaks existing source files.
9
10
  */
@@ -43,8 +44,15 @@ var __importStar = (this && this.__importStar) || (function () {
43
44
  Object.defineProperty(exports, "__esModule", { value: true });
44
45
  const fs = __importStar(require("fs"));
45
46
  const path = __importStar(require("path"));
46
- const compile_1 = require("../compile");
47
+ const child_process_1 = require("child_process");
48
+ const os = __importStar(require("os"));
47
49
  const REPO_ROOT = path.resolve(__dirname, '../../');
50
+ const CLI = path.join(REPO_ROOT, 'dist', 'cli.js');
51
+ // Ensure dist/cli.js exists — build first if not (e.g. in CI)
52
+ if (!fs.existsSync(CLI)) {
53
+ console.log('[compile-all] dist/cli.js not found, running npm run build...');
54
+ (0, child_process_1.execSync)('npm run build', { cwd: REPO_ROOT, stdio: 'inherit' });
55
+ }
48
56
  /** Patterns to skip */
49
57
  const SKIP_GLOBS = [
50
58
  'node_modules',
@@ -72,18 +80,28 @@ function findMcrsFiles(dir) {
72
80
  return results;
73
81
  }
74
82
  const mcrsFiles = findMcrsFiles(REPO_ROOT);
75
- describe('compile-all: every .mcrs file should compile without errors', () => {
83
+ const TMP_OUT = path.join(os.tmpdir(), 'redscript-compile-all');
84
+ describe('compile-all: every .mcrs file should compile without errors (CLI)', () => {
76
85
  test('found at least one .mcrs file', () => {
77
86
  expect(mcrsFiles.length).toBeGreaterThan(0);
78
87
  });
79
88
  for (const filePath of mcrsFiles) {
80
89
  const label = path.relative(REPO_ROOT, filePath);
81
90
  test(label, () => {
82
- const source = fs.readFileSync(filePath, 'utf8');
83
- // Should not throw
84
- expect(() => {
85
- (0, compile_1.compile)(source, { namespace: 'smoke_test', optimize: false });
86
- }).not.toThrow();
91
+ const outDir = path.join(TMP_OUT, label.replace(/[^a-zA-Z0-9]/g, '_'));
92
+ let stdout = '';
93
+ let stderr = '';
94
+ try {
95
+ const result = (0, child_process_1.execSync)(`node "${CLI}" compile "${filePath}" -o "${outDir}"`, { encoding: 'utf8', stdio: 'pipe' });
96
+ stdout = result;
97
+ }
98
+ catch (err) {
99
+ stdout = err.stdout ?? '';
100
+ stderr = err.stderr ?? '';
101
+ const output = (stdout + stderr).trim();
102
+ // Fail with the compiler error message
103
+ throw new Error(`Compile failed for ${label}:\n${output}`);
104
+ }
87
105
  });
88
106
  }
89
107
  });
package/dist/cli.js CHANGED
@@ -51,6 +51,8 @@ const repl_1 = require("./repl");
51
51
  const metadata_1 = require("./builtins/metadata");
52
52
  const fs = __importStar(require("fs"));
53
53
  const path = __importStar(require("path"));
54
+ const https = __importStar(require("https"));
55
+ const child_process_1 = require("child_process");
54
56
  // Parse command line arguments
55
57
  const args = process.argv.slice(2);
56
58
  function printUsage() {
@@ -74,6 +76,7 @@ Commands:
74
76
  generate-dts Generate builtin function declaration file (builtins.d.mcrs)
75
77
  repl Start an interactive RedScript REPL
76
78
  version Print the RedScript version
79
+ upgrade Upgrade to the latest version (npm install -g redscript-mc@latest)
77
80
 
78
81
  Options:
79
82
  -o, --output <path> Output directory or file path, depending on target
@@ -92,15 +95,90 @@ Targets:
92
95
  structure Generate a Minecraft structure .nbt file with command blocks
93
96
  `);
94
97
  }
95
- function printVersion() {
98
+ function getLocalVersion() {
96
99
  const packagePath = path.join(__dirname, '..', 'package.json');
97
100
  try {
98
101
  const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
99
- console.log(`RedScript v${pkg.version}`);
102
+ return pkg.version ?? '0.0.0';
100
103
  }
101
104
  catch {
102
- console.log('RedScript v0.1.0');
105
+ return '0.0.0';
106
+ }
107
+ }
108
+ function printVersion() {
109
+ console.log(`RedScript v${getLocalVersion()}`);
110
+ }
111
+ /** Fetch latest version from npm registry (non-blocking, best-effort). */
112
+ function fetchLatestVersion() {
113
+ return new Promise(resolve => {
114
+ const req = https.get('https://registry.npmjs.org/redscript-mc/latest', { timeout: 3000 }, res => {
115
+ let data = '';
116
+ res.on('data', chunk => { data += chunk; });
117
+ res.on('end', () => {
118
+ try {
119
+ const json = JSON.parse(data);
120
+ resolve(json.version ?? null);
121
+ }
122
+ catch {
123
+ resolve(null);
124
+ }
125
+ });
126
+ });
127
+ req.on('error', () => resolve(null));
128
+ req.on('timeout', () => { req.destroy(); resolve(null); });
129
+ });
130
+ }
131
+ /** Compare semver strings. Returns true if b > a. */
132
+ function isNewer(current, latest) {
133
+ const parse = (v) => v.replace(/^v/, '').split('.').map(Number);
134
+ const [ca, cb, cc] = parse(current);
135
+ const [la, lb, lc] = parse(latest);
136
+ if (la !== ca)
137
+ return la > ca;
138
+ if (lb !== cb)
139
+ return lb > cb;
140
+ return lc > cc;
141
+ }
142
+ /**
143
+ * Check for a newer version and print a notice if one exists.
144
+ * Runs in background — does NOT block normal CLI operation.
145
+ */
146
+ async function checkForUpdates(silent = false) {
147
+ const current = getLocalVersion();
148
+ const latest = await fetchLatestVersion();
149
+ if (latest && isNewer(current, latest)) {
150
+ console.log(`\n💡 New version available: v${current} → v${latest}`);
151
+ console.log(` Run: redscript upgrade\n`);
103
152
  }
153
+ else if (!silent && latest) {
154
+ // Only print when explicitly running 'version' or 'upgrade'
155
+ // No output for normal commands — keep startup noise-free
156
+ }
157
+ }
158
+ /** Run npm install -g to upgrade to latest. */
159
+ function upgradeCommand() {
160
+ const current = getLocalVersion();
161
+ console.log(`Current version: v${current}`);
162
+ console.log('Checking latest version...');
163
+ fetchLatestVersion().then(latest => {
164
+ if (!latest) {
165
+ console.error('Could not fetch latest version from npm.');
166
+ process.exit(1);
167
+ }
168
+ if (!isNewer(current, latest)) {
169
+ console.log(`✅ Already up to date (v${current})`);
170
+ return;
171
+ }
172
+ console.log(`Upgrading v${current} → v${latest}...`);
173
+ try {
174
+ (0, child_process_1.execSync)('npm install -g redscript-mc@latest', { stdio: 'inherit' });
175
+ console.log(`✅ Upgraded to v${latest}`);
176
+ }
177
+ catch {
178
+ console.error('Upgrade failed. Try manually: npm install -g redscript-mc@latest');
179
+ process.exit(1);
180
+ }
181
+ });
104
182
  }
105
183
  function parseArgs(args) {
106
184
  const result = { dce: true };
@@ -163,7 +241,13 @@ function printWarnings(warnings) {
163
241
  return;
164
242
  }
165
243
  for (const warning of warnings) {
166
- console.error(`Warning [${warning.code}]: ${warning.message}`);
244
+ const loc = warning.filePath
245
+ ? `${warning.filePath}:${warning.line ?? '?'}`
246
+ : warning.line != null
247
+ ? `line ${warning.line}`
248
+ : null;
249
+ const locStr = loc ? ` (${loc})` : '';
250
+ console.error(`Warning [${warning.code}]: ${warning.message}${locStr}`);
167
251
  }
168
252
  }
169
253
  function formatReduction(before, after) {
@@ -369,6 +453,12 @@ async function main() {
369
453
  printUsage();
370
454
  process.exit(parsed.help ? 0 : 1);
371
455
  }
456
+ // Background update check — non-blocking, only shows notice if newer version exists
457
+ // Skip for repl/upgrade/version to avoid double-printing
458
+ const noCheckCmds = new Set(['upgrade', 'update', 'version', 'repl']);
459
+ if (!noCheckCmds.has(parsed.command ?? '')) {
460
+ checkForUpdates().catch(() => { });
461
+ }
372
462
  switch (parsed.command) {
373
463
  case 'compile':
374
464
  if (!parsed.file) {
@@ -429,6 +519,11 @@ async function main() {
429
519
  break;
430
520
  case 'version':
431
521
  printVersion();
522
+ await checkForUpdates();
523
+ break;
524
+ case 'upgrade':
525
+ case 'update':
526
+ upgradeCommand();
432
527
  break;
433
528
  default:
434
529
  console.error(`Error: Unknown command '${parsed.command}'`);
package/dist/compile.d.ts CHANGED
@@ -30,6 +30,14 @@ export interface PreprocessedSource {
30
30
  source: string;
31
31
  ranges: SourceRange[];
32
32
  }
33
+ /**
34
+ * Resolve a combined-source line number back to the original file and line.
35
+ * Returns { filePath, line } if a mapping is found, otherwise returns the input unchanged.
36
+ */
37
+ export declare function resolveSourceLine(combinedLine: number, ranges: SourceRange[], fallbackFile?: string): {
38
+ filePath?: string;
39
+ line: number;
40
+ };
33
41
  interface PreprocessOptions {
34
42
  filePath?: string;
35
43
  seen?: Set<string>;
package/dist/compile.js CHANGED
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.resolveSourceLine = resolveSourceLine;
41
42
  exports.preprocessSourceWithMetadata = preprocessSourceWithMetadata;
42
43
  exports.preprocessSource = preprocessSource;
43
44
  exports.compile = compile;
@@ -51,6 +52,19 @@ const passes_1 = require("./optimizer/passes");
51
52
  const dce_1 = require("./optimizer/dce");
52
53
  const mcfunction_1 = require("./codegen/mcfunction");
53
54
  const diagnostics_1 = require("./diagnostics");
55
+ /**
56
+ * Resolve a combined-source line number back to the original file and line.
57
+ * Returns { filePath, line } if a mapping is found, otherwise returns the input unchanged.
58
+ */
59
+ function resolveSourceLine(combinedLine, ranges, fallbackFile) {
60
+ for (const range of ranges) {
61
+ if (combinedLine >= range.startLine && combinedLine <= range.endLine) {
62
+ const localLine = combinedLine - range.startLine + 1;
63
+ return { filePath: range.filePath, line: localLine };
64
+ }
65
+ }
66
+ return { filePath: fallbackFile, line: combinedLine };
67
+ }
54
68
  const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/;
55
69
  function countLines(source) {
56
70
  return source === '' ? 0 : source.split('\n').length;
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ function compile(source, options = {}) {
38
38
  const tokens = new lexer_1.Lexer(preprocessedSource, filePath).tokenize();
39
39
  // Parsing
40
40
  const parsedAst = new parser_1.Parser(tokens, preprocessedSource, filePath).parse(namespace);
41
- const dceResult = shouldRunDce ? (0, dce_1.eliminateDeadCode)(parsedAst) : { program: parsedAst, warnings: [] };
41
+ const dceResult = shouldRunDce ? (0, dce_1.eliminateDeadCode)(parsedAst, preprocessed.ranges) : { program: parsedAst, warnings: [] };
42
42
  const ast = dceResult.program;
43
43
  // Type checking (warn mode - collect errors but don't block)
44
44
  let typeErrors;
@@ -4,6 +4,7 @@ export interface DCEWarning {
4
4
  code: string;
5
5
  line?: number;
6
6
  col?: number;
7
+ filePath?: string;
7
8
  }
8
9
  export declare class DeadCodeEliminator {
9
10
  private readonly functionMap;
@@ -27,7 +28,7 @@ export declare class DeadCodeEliminator {
27
28
  private transformStmt;
28
29
  private transformExpr;
29
30
  }
30
- export declare function eliminateDeadCode(program: Program): {
31
+ export declare function eliminateDeadCode(program: Program, sourceRanges?: import('../compile').SourceRange[]): {
31
32
  program: Program;
32
33
  warnings: DCEWarning[];
33
34
  };
@@ -604,9 +604,20 @@ class DeadCodeEliminator {
604
604
  }
605
605
  }
606
606
  exports.DeadCodeEliminator = DeadCodeEliminator;
607
- function eliminateDeadCode(program) {
607
+ function eliminateDeadCode(program, sourceRanges) {
608
608
  const eliminator = new DeadCodeEliminator();
609
609
  const result = eliminator.eliminate(program);
610
- return { program: result, warnings: eliminator.warnings };
610
+ let warnings = eliminator.warnings;
611
+ // Resolve combined-source line numbers back to original file + line
612
+ if (sourceRanges && sourceRanges.length > 0) {
613
+ const { resolveSourceLine } = require('../compile');
614
+ warnings = warnings.map(w => {
615
+ if (w.line == null)
616
+ return w;
617
+ const resolved = resolveSourceLine(w.line, sourceRanges);
618
+ return { ...w, line: resolved.line, filePath: resolved.filePath ?? w.filePath };
619
+ });
620
+ }
621
+ return { program: result, warnings };
611
622
  }
612
623
  //# sourceMappingURL=dce.js.map
@@ -1193,10 +1193,22 @@ var require_parser = __commonJS({
1193
1193
  parseForRangeStmt(forToken) {
1194
1194
  const varName = this.expect("ident").value;
1195
1195
  this.expect("in");
1196
- const rangeToken = this.expect("range_lit");
1197
- const range = this.parseRangeValue(rangeToken.value);
1198
- const start = this.withLoc({ kind: "int_lit", value: range.min ?? 0 }, rangeToken);
1199
- const end = this.withLoc({ kind: "int_lit", value: range.max ?? 0 }, rangeToken);
1196
+ let start;
1197
+ let end;
1198
+ if (this.check("range_lit")) {
1199
+ const rangeToken = this.advance();
1200
+ const range = this.parseRangeValue(rangeToken.value);
1201
+ start = this.withLoc({ kind: "int_lit", value: range.min ?? 0 }, rangeToken);
1202
+ if (range.max !== null && range.max !== void 0) {
1203
+ end = this.withLoc({ kind: "int_lit", value: range.max }, rangeToken);
1204
+ } else {
1205
+ end = this.parseUnaryExpr();
1206
+ }
1207
+ } else {
1208
+ start = this.withLoc({ kind: "int_lit", value: 0 }, this.peek());
1209
+ end = this.withLoc({ kind: "int_lit", value: 0 }, this.peek());
1210
+ this.error("Dynamic range start requires a literal integer (e.g. 0..count)");
1211
+ }
1200
1212
  const body = this.parseBlock();
1201
1213
  return this.withLoc({ kind: "for_range", varName, start, end, body }, forToken);
1202
1214
  }
@@ -1656,6 +1668,19 @@ var require_parser = __commonJS({
1656
1668
  this.error(`Unexpected token '${token.kind}'`);
1657
1669
  }
1658
1670
  parseLiteralExpr() {
1671
+ if (this.check("-")) {
1672
+ this.advance();
1673
+ const token = this.peek();
1674
+ if (token.kind === "int_lit") {
1675
+ this.advance();
1676
+ return this.withLoc({ kind: "int_lit", value: -Number(token.value) }, token);
1677
+ }
1678
+ if (token.kind === "float_lit") {
1679
+ this.advance();
1680
+ return this.withLoc({ kind: "float_lit", value: -Number(token.value) }, token);
1681
+ }
1682
+ this.error("Expected number after unary -");
1683
+ }
1659
1684
  const expr = this.parsePrimaryExpr();
1660
1685
  if (expr.kind === "int_lit" || expr.kind === "float_lit" || expr.kind === "bool_lit" || expr.kind === "str_lit") {
1661
1686
  return expr;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "redscript-vscode",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "redscript-vscode",
9
- "version": "1.0.11",
9
+ "version": "1.0.12",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "redscript": "file:../../"
@@ -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.11",
5
+ "version": "1.0.12",
6
6
  "publisher": "bkmashiro",
7
7
  "icon": "icon.png",
8
8
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redscript-mc",
3
- "version": "1.2.19",
3
+ "version": "1.2.21",
4
4
  "description": "A high-level programming language that compiles to Minecraft datapacks",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -2,16 +2,25 @@
2
2
  * Compile-all smoke test
3
3
  *
4
4
  * Finds every .mcrs file in the repo (excluding declaration files and node_modules)
5
- * and verifies that each one compiles without throwing an error.
5
+ * and verifies that each one compiles without errors via the CLI (which handles
6
+ * `import` statements, unlike the bare `compile()` function).
6
7
  *
7
8
  * This catches regressions where a language change breaks existing source files.
8
9
  */
9
10
 
10
11
  import * as fs from 'fs'
11
12
  import * as path from 'path'
12
- import { compile } from '../compile'
13
+ import { execSync } from 'child_process'
14
+ import * as os from 'os'
13
15
 
14
16
  const REPO_ROOT = path.resolve(__dirname, '../../')
17
+ const CLI = path.join(REPO_ROOT, 'dist', 'cli.js')
18
+
19
+ // Ensure dist/cli.js exists — build first if not (e.g. in CI)
20
+ if (!fs.existsSync(CLI)) {
21
+ console.log('[compile-all] dist/cli.js not found, running npm run build...')
22
+ execSync('npm run build', { cwd: REPO_ROOT, stdio: 'inherit' })
23
+ }
15
24
 
16
25
  /** Patterns to skip */
17
26
  const SKIP_GLOBS = [
@@ -41,8 +50,9 @@ function findMcrsFiles(dir: string): string[] {
41
50
  }
42
51
 
43
52
  const mcrsFiles = findMcrsFiles(REPO_ROOT)
53
+ const TMP_OUT = path.join(os.tmpdir(), 'redscript-compile-all')
44
54
 
45
- describe('compile-all: every .mcrs file should compile without errors', () => {
55
+ describe('compile-all: every .mcrs file should compile without errors (CLI)', () => {
46
56
  test('found at least one .mcrs file', () => {
47
57
  expect(mcrsFiles.length).toBeGreaterThan(0)
48
58
  })
@@ -50,11 +60,22 @@ describe('compile-all: every .mcrs file should compile without errors', () => {
50
60
  for (const filePath of mcrsFiles) {
51
61
  const label = path.relative(REPO_ROOT, filePath)
52
62
  test(label, () => {
53
- const source = fs.readFileSync(filePath, 'utf8')
54
- // Should not throw
55
- expect(() => {
56
- compile(source, { namespace: 'smoke_test', optimize: false })
57
- }).not.toThrow()
63
+ const outDir = path.join(TMP_OUT, label.replace(/[^a-zA-Z0-9]/g, '_'))
64
+ let stdout = ''
65
+ let stderr = ''
66
+ try {
67
+ const result = execSync(
68
+ `node "${CLI}" compile "${filePath}" -o "${outDir}"`,
69
+ { encoding: 'utf8', stdio: 'pipe' }
70
+ )
71
+ stdout = result
72
+ } catch (err: any) {
73
+ stdout = err.stdout ?? ''
74
+ stderr = err.stderr ?? ''
75
+ const output = (stdout + stderr).trim()
76
+ // Fail with the compiler error message
77
+ throw new Error(`Compile failed for ${label}:\n${output}`)
78
+ }
58
79
  })
59
80
  }
60
81
  })
package/src/cli.ts CHANGED
@@ -18,6 +18,8 @@ import { generateDts } from './builtins/metadata'
18
18
  import type { OptimizationStats } from './optimizer/commands'
19
19
  import * as fs from 'fs'
20
20
  import * as path from 'path'
21
+ import * as https from 'https'
22
+ import { execSync } from 'child_process'
21
23
 
22
24
  // Parse command line arguments
23
25
  const args = process.argv.slice(2)
@@ -43,6 +45,7 @@ Commands:
43
45
  generate-dts Generate builtin function declaration file (builtins.d.mcrs)
44
46
  repl Start an interactive RedScript REPL
45
47
  version Print the RedScript version
48
+ upgrade Upgrade to the latest version (npm install -g redscript-mc@latest)
46
49
 
47
50
  Options:
48
51
  -o, --output <path> Output directory or file path, depending on target
@@ -62,16 +65,96 @@ Targets:
62
65
  `)
63
66
  }
64
67
 
65
- function printVersion(): void {
68
+ function getLocalVersion(): string {
66
69
  const packagePath = path.join(__dirname, '..', 'package.json')
67
70
  try {
68
71
  const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'))
69
- console.log(`RedScript v${pkg.version}`)
72
+ return pkg.version ?? '0.0.0'
70
73
  } catch {
71
- console.log('RedScript v0.1.0')
74
+ return '0.0.0'
75
+ }
76
+ }
77
+
78
+ function printVersion(): void {
79
+ console.log(`RedScript v${getLocalVersion()}`)
80
+ }
81
+
82
+ /** Fetch latest version from npm registry (non-blocking, best-effort). */
83
+ function fetchLatestVersion(): Promise<string | null> {
84
+ return new Promise(resolve => {
85
+ const req = https.get(
86
+ 'https://registry.npmjs.org/redscript-mc/latest',
87
+ { timeout: 3000 },
88
+ res => {
89
+ let data = ''
90
+ res.on('data', chunk => { data += chunk })
91
+ res.on('end', () => {
92
+ try {
93
+ const json = JSON.parse(data)
94
+ resolve(json.version ?? null)
95
+ } catch {
96
+ resolve(null)
97
+ }
98
+ })
99
+ }
100
+ )
101
+ req.on('error', () => resolve(null))
102
+ req.on('timeout', () => { req.destroy(); resolve(null) })
103
+ })
104
+ }
105
+
106
+ /** Compare semver strings. Returns true if b > a. */
107
+ function isNewer(current: string, latest: string): boolean {
108
+ const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number)
109
+ const [ca, cb, cc] = parse(current)
110
+ const [la, lb, lc] = parse(latest)
111
+ if (la !== ca) return la > ca
112
+ if (lb !== cb) return lb > cb
113
+ return lc > cc
114
+ }
115
+
116
+ /**
117
+ * Check for a newer version and print a notice if one exists.
118
+ * Runs in background — does NOT block normal CLI operation.
119
+ */
120
+ async function checkForUpdates(silent = false): Promise<void> {
121
+ const current = getLocalVersion()
122
+ const latest = await fetchLatestVersion()
123
+ if (latest && isNewer(current, latest)) {
124
+ console.log(`\n💡 New version available: v${current} → v${latest}`)
125
+ console.log(` Run: redscript upgrade\n`)
126
+ } else if (!silent && latest) {
127
+ // Only print when explicitly running 'version' or 'upgrade'
128
+ // No output for normal commands — keep startup noise-free
72
129
  }
73
130
  }
74
131
 
132
+ /** Run npm install -g to upgrade to latest. */
133
+ function upgradeCommand(): void {
134
+ const current = getLocalVersion()
135
+ console.log(`Current version: v${current}`)
136
+ console.log('Checking latest version...')
137
+
138
+ fetchLatestVersion().then(latest => {
139
+ if (!latest) {
140
+ console.error('Could not fetch latest version from npm.')
141
+ process.exit(1)
142
+ }
143
+ if (!isNewer(current, latest)) {
144
+ console.log(`✅ Already up to date (v${current})`)
145
+ return
146
+ }
147
+ console.log(`Upgrading v${current} → v${latest}...`)
148
+ try {
149
+ execSync('npm install -g redscript-mc@latest', { stdio: 'inherit' })
150
+ console.log(`✅ Upgraded to v${latest}`)
151
+ } catch {
152
+ console.error('Upgrade failed. Try manually: npm install -g redscript-mc@latest')
153
+ process.exit(1)
154
+ }
155
+ })
156
+ }
157
+
75
158
  function parseArgs(args: string[]): {
76
159
  command?: string
77
160
  file?: string
@@ -134,13 +217,19 @@ function deriveNamespace(filePath: string): string {
134
217
  return basename.toLowerCase().replace(/[^a-z0-9]/g, '_')
135
218
  }
136
219
 
137
- function printWarnings(warnings: Array<{ code: string; message: string }> | undefined): void {
220
+ function printWarnings(warnings: Array<{ code: string; message: string; line?: number; col?: number; filePath?: string }> | undefined): void {
138
221
  if (!warnings || warnings.length === 0) {
139
222
  return
140
223
  }
141
224
 
142
225
  for (const warning of warnings) {
143
- console.error(`Warning [${warning.code}]: ${warning.message}`)
226
+ const loc = warning.filePath
227
+ ? `${warning.filePath}:${warning.line ?? '?'}`
228
+ : warning.line != null
229
+ ? `line ${warning.line}`
230
+ : null
231
+ const locStr = loc ? ` (${loc})` : ''
232
+ console.error(`Warning [${warning.code}]: ${warning.message}${locStr}`)
144
233
  }
145
234
  }
146
235
 
@@ -378,6 +467,13 @@ async function main(): Promise<void> {
378
467
  process.exit(parsed.help ? 0 : 1)
379
468
  }
380
469
 
470
+ // Background update check — non-blocking, only shows notice if newer version exists
471
+ // Skip for repl/upgrade/version to avoid double-printing
472
+ const noCheckCmds = new Set(['upgrade', 'update', 'version', 'repl'])
473
+ if (!noCheckCmds.has(parsed.command ?? '')) {
474
+ checkForUpdates().catch(() => { /* ignore */ })
475
+ }
476
+
381
477
  switch (parsed.command) {
382
478
  case 'compile':
383
479
  if (!parsed.file) {
@@ -458,6 +554,12 @@ async function main(): Promise<void> {
458
554
 
459
555
  case 'version':
460
556
  printVersion()
557
+ await checkForUpdates()
558
+ break
559
+
560
+ case 'upgrade':
561
+ case 'update':
562
+ upgradeCommand()
461
563
  break
462
564
 
463
565
  default:
package/src/compile.ts CHANGED
@@ -52,6 +52,24 @@ export interface PreprocessedSource {
52
52
  ranges: SourceRange[]
53
53
  }
54
54
 
55
+ /**
56
+ * Resolve a combined-source line number back to the original file and line.
57
+ * Returns { filePath, line } if a mapping is found, otherwise returns the input unchanged.
58
+ */
59
+ export function resolveSourceLine(
60
+ combinedLine: number,
61
+ ranges: SourceRange[],
62
+ fallbackFile?: string
63
+ ): { filePath?: string; line: number } {
64
+ for (const range of ranges) {
65
+ if (combinedLine >= range.startLine && combinedLine <= range.endLine) {
66
+ const localLine = combinedLine - range.startLine + 1
67
+ return { filePath: range.filePath, line: localLine }
68
+ }
69
+ }
70
+ return { filePath: fallbackFile, line: combinedLine }
71
+ }
72
+
55
73
  const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/
56
74
 
57
75
  interface PreprocessOptions {
@@ -204,7 +204,7 @@ fn check_border_damage() {
204
204
  // 需要检查玩家是否在边界外
205
205
  foreach (p in @a) {
206
206
  // 简化:使用 execute positioned
207
- let dist: int = 0; // 需要计算距离中心的距离
207
+ let _dist: int = 0; // TODO: calculate distance from center
208
208
  // 如果超出边界,造成伤害
209
209
  // effect(p, "minecraft:wither", 1, 0);
210
210
  }
@@ -1,8 +1,8 @@
1
1
  // ============================================
2
- // Zombie Survival - 僵尸生存模式
2
+ // Zombie Survival - Zombie Survival Mode
3
3
  // ============================================
4
- // 场景:玩家在竞技场中心抵御一波波僵尸
5
- // 每波结束后可以购买装备升级
4
+ // Scenario: Players defend against waves of zombies in the arena center
5
+ // Players can purchase equipment upgrades between waves
6
6
  // Scenario: Survive zombie waves, buy upgrades between rounds
7
7
  // ============================================
8
8
 
@@ -12,19 +12,19 @@ import "../stdlib/inventory.mcrs"
12
12
  import "../stdlib/bossbar.mcrs"
13
13
  import "../stdlib/particles.mcrs"
14
14
 
15
- // ===== 配置 =====
15
+ // ===== Configuration =====
16
16
  const ARENA_X: int = 0;
17
17
  const ARENA_Y: int = 64;
18
18
  const ARENA_Z: int = 0;
19
19
  const ARENA_RADIUS: int = 30;
20
- const WAVE_DELAY: int = 200; // 10秒准备时间
20
+ const WAVE_DELAY: int = 200; // 10-second preparation time
21
21
 
22
- // ===== 游戏状态 =====
22
+ // ===== Game State =====
23
23
  struct SurvivalState {
24
24
  running: int,
25
25
  wave: int,
26
26
  zombies_left: int,
27
- phase: int, // 0=准备, 1=战斗
27
+ phase: int, // 0=prep, 1=combat
28
28
  prep_timer: int,
29
29
  total_kills: int
30
30
  }
@@ -38,33 +38,33 @@ let state: SurvivalState = {
38
38
  total_kills: 0
39
39
  };
40
40
 
41
- // ===== 玩家数据 =====
42
- // 金币记分板: zs_coins
43
- // 击杀数: zs_kills
41
+ // ===== Player Data =====
42
+ // Coin scoreboard: zs_coins
43
+ // Kill count: zs_kills
44
44
 
45
- // ===== 初始化 =====
45
+ // ===== Initialization =====
46
46
  @load
47
47
  fn init() {
48
48
  scoreboard_add_objective("zs_coins", "dummy");
49
49
  scoreboard_add_objective("zs_kills", "dummy");
50
50
  scoreboard_add_objective("zs_display", "dummy");
51
51
 
52
- // 显示记分板
52
+ // Display scoreboard
53
53
  scoreboard_display("sidebar", "zs_display");
54
54
 
55
55
  set_night();
56
56
  disable_mob_griefing();
57
57
 
58
- announce("§4[僵尸生存] §f已加载!输入 /trigger start 开始");
58
+ announce("§4[Zombie Survival] §fLoaded! Type /trigger start to begin");
59
59
  }
60
60
 
61
- // ===== 开始游戏 =====
61
+ // ===== Start Game =====
62
62
  fn start_game() {
63
63
  state.running = 1;
64
64
  state.wave = 0;
65
65
  state.total_kills = 0;
66
66
 
67
- // 初始化玩家
67
+ // Initialize players
68
68
  foreach (p in @a) {
69
69
  scoreboard_set(p, "zs_coins", 0);
70
70
  scoreboard_set(p, "zs_kills", 0);
@@ -74,74 +74,74 @@ fn start_game() {
74
74
  tp(p, ARENA_X, ARENA_Y, ARENA_Z);
75
75
  }
76
76
 
77
- // 创建 bossbar
78
- create_progress_bar("zs_wave", "§c僵尸剩余", 10);
77
+ // Create bossbar
78
+ create_progress_bar("zs_wave", "§cZombies Remaining", 10);
79
79
 
80
- title(@a, "§4僵尸生存");
81
- subtitle(@a, "§7准备战斗...");
80
+ title(@a, "§4Zombie Survival");
81
+ subtitle(@a, "§7Prepare for battle...");
82
82
 
83
- // 开始第一波
83
+ // Start first wave
84
84
  start_prep_phase();
85
85
  }
86
86
 
87
- // ===== 阶段控制 =====
87
+ // ===== Phase Control =====
88
88
  fn start_prep_phase() {
89
89
  state.phase = 0;
90
90
  state.prep_timer = WAVE_DELAY;
91
91
  state.wave = state.wave + 1;
92
92
 
93
- announce("§4[僵尸生存] §e第 " + state.wave + " 波即将来袭!");
94
- announce("§7准备时间 10 秒...");
93
+ announce("§4[Zombie Survival] §eWave " + state.wave + " incoming!");
94
+ announce("§7Preparation time: 10 seconds...");
95
95
 
96
- // 商店提示
96
+ // Shop hint
97
97
  if (state.wave > 1) {
98
- announce("§a[商店] §f输入 /trigger buy 购买装备");
98
+ announce("§a[Shop] §fType /trigger buy to purchase equipment");
99
99
  }
100
100
  }
101
101
 
102
102
  fn start_combat_phase() {
103
103
  state.phase = 1;
104
104
 
105
- // 计算僵尸数量 (每波增加)
105
+ // Calculate zombie count (increases each wave)
106
106
  let zombie_count: int = 3 + (state.wave * 2);
107
107
  state.zombies_left = zombie_count;
108
108
 
109
- // 更新 bossbar
109
+ // Update bossbar
110
110
  bossbar_set_max("zs_wave", zombie_count);
111
111
  bossbar_set_value("zs_wave", zombie_count);
112
112
 
113
- title(@a, "§c第 " + state.wave + " 波");
113
+ title(@a, "§cWave " + state.wave);
114
114
 
115
- // 生成僵尸
115
+ // Spawn zombies
116
116
  spawn_zombies(zombie_count);
117
117
  }
118
118
 
119
119
  fn spawn_zombies(count: int) {
120
120
  for i in 0..count {
121
- // 在竞技场边缘随机生成
122
- let angle: int = i * 30; // 分散生成
121
+ // Spawn randomly at arena edge
122
+ let _angle: int = i * 30; // spread spawning
123
123
  let spawn_x: int = ARENA_X + (ARENA_RADIUS - 5);
124
124
  let spawn_z: int = ARENA_Z;
125
125
 
126
- // 根据波数增加僵尸强度
126
+ // Increase zombie difficulty based on wave number
127
127
  if (state.wave < 3) {
128
128
  summon("minecraft:zombie", spawn_x, ARENA_Y, spawn_z);
129
129
  } else {
130
130
  if (state.wave < 5) {
131
- // 穿盔甲的僵尸
131
+ // Armored zombies
132
132
  summon("minecraft:zombie", spawn_x, ARENA_Y, spawn_z,
133
133
  {ArmorItems: [{}, {}, {id: "iron_chestplate", Count: 1}, {}]});
134
134
  } else {
135
- // 快速僵尸
135
+ // Fast zombies
136
136
  summon("minecraft:husk", spawn_x, ARENA_Y, spawn_z);
137
137
  }
138
138
  }
139
139
  }
140
140
 
141
- announce("§c" + count + " 只僵尸出现了!");
141
+ announce("§c" + count + " zombies have appeared!");
142
142
  }
143
143
 
144
- // ===== tick =====
144
+ // ===== Every Tick =====
145
145
  @tick
146
146
  fn game_tick() {
147
147
  if (state.running == 0) {
@@ -149,10 +149,10 @@ fn game_tick() {
149
149
  }
150
150
 
151
151
  if (state.phase == 0) {
152
- // 准备阶段
152
+ // Preparation phase
153
153
  prep_tick();
154
154
  } else {
155
- // 战斗阶段
155
+ // Combat phase
156
156
  combat_tick();
157
157
  }
158
158
 
@@ -162,15 +162,15 @@ fn game_tick() {
162
162
  fn prep_tick() {
163
163
  state.prep_timer = state.prep_timer - 1;
164
164
 
165
- // 倒计时提示
165
+ // Countdown hints
166
166
  if (state.prep_timer == 100) {
167
- actionbar(@a, "§e5 秒...");
167
+ actionbar(@a, "§e5 seconds...");
168
168
  }
169
169
  if (state.prep_timer == 60) {
170
- actionbar(@a, "§e3 秒...");
170
+ actionbar(@a, "§e3 seconds...");
171
171
  }
172
172
  if (state.prep_timer == 20) {
173
- actionbar(@a, "§c1 秒...");
173
+ actionbar(@a, "§c1 second...");
174
174
  }
175
175
 
176
176
  if (state.prep_timer <= 0) {
@@ -179,20 +179,20 @@ fn prep_tick() {
179
179
  }
180
180
 
181
181
  fn combat_tick() {
182
- // 检查僵尸数量
182
+ // Check zombie count
183
183
  let zombies: int = count_entities(@e[type=zombie, distance=..50]);
184
184
  let husks: int = count_entities(@e[type=husk, distance=..50]);
185
185
  state.zombies_left = zombies + husks;
186
186
 
187
- // 更新 bossbar
187
+ // Update bossbar
188
188
  update_bar("zs_wave", state.zombies_left);
189
189
 
190
- // 检查波次完成
190
+ // Check wave completion
191
191
  if (state.zombies_left <= 0) {
192
192
  wave_complete();
193
193
  }
194
194
 
195
- // 检查玩家存活
195
+ // Check player survival
196
196
  let alive: int = count_entities(@a[gamemode=survival]);
197
197
  if (alive <= 0) {
198
198
  game_over();
@@ -200,17 +200,17 @@ fn combat_tick() {
200
200
  }
201
201
 
202
202
  fn wave_complete() {
203
- announce("§4[僵尸生存] §a第 " + state.wave + " 波完成!");
203
+ announce("§4[Zombie Survival] §aWave " + state.wave + " complete!");
204
204
 
205
- // 奖励金币
205
+ // Reward coins
206
206
  let reward: int = 50 + (state.wave * 25);
207
207
  foreach (p in @a) {
208
208
  scoreboard_add(p, "zs_coins", reward);
209
209
  happy(p);
210
210
  }
211
- announce("§6+" + reward + " 金币");
211
+ announce("§6+" + reward + " coins");
212
212
 
213
- // 检查特殊波
213
+ // Check special wave
214
214
  if (state.wave == 10) {
215
215
  victory();
216
216
  return;
@@ -219,65 +219,65 @@ fn wave_complete() {
219
219
  start_prep_phase();
220
220
  }
221
221
 
222
- // ===== 商店系统 =====
222
+ // ===== Shop System =====
223
223
  fn buy_item(player: selector, item_id: int) {
224
224
  let coins: int = scoreboard_get(player, "zs_coins");
225
225
 
226
226
  if (item_id == 1) {
227
- // 铁剑 - 100 金币
227
+ // Iron sword - 100 coins
228
228
  if (coins >= 100) {
229
229
  scoreboard_add(player, "zs_coins", -100);
230
230
  give(player, "minecraft:iron_sword", 1);
231
- tell(player, "§a购买成功:铁剑");
231
+ tell(player, "§aPurchased: Iron Sword");
232
232
  } else {
233
- tell(player, "§c金币不足!需要 100");
233
+ tell(player, "§cNot enough coins! Need 100");
234
234
  }
235
235
  }
236
236
 
237
237
  if (item_id == 2) {
238
- // 铁甲 - 200 金币
238
+ // Iron armor - 200 coins
239
239
  if (coins >= 200) {
240
240
  scoreboard_add(player, "zs_coins", -200);
241
241
  give(player, "minecraft:iron_chestplate", 1);
242
242
  give(player, "minecraft:iron_leggings", 1);
243
243
  give(player, "minecraft:iron_boots", 1);
244
- tell(player, "§a购买成功:铁甲套装");
244
+ tell(player, "§aPurchased: Iron Armor Set");
245
245
  } else {
246
- tell(player, "§c金币不足!需要 200");
246
+ tell(player, "§cNot enough coins! Need 200");
247
247
  }
248
248
  }
249
249
 
250
250
  if (item_id == 3) {
251
- // 弓箭 - 150 金币
251
+ // Bow and arrows - 150 coins
252
252
  if (coins >= 150) {
253
253
  scoreboard_add(player, "zs_coins", -150);
254
254
  give(player, "minecraft:bow", 1);
255
255
  give(player, "minecraft:arrow", 32);
256
- tell(player, "§a购买成功:弓 + 32 ");
256
+ tell(player, "§aPurchased: Bow + 32 Arrows");
257
257
  } else {
258
- tell(player, "§c金币不足!需要 150");
258
+ tell(player, "§cNot enough coins! Need 150");
259
259
  }
260
260
  }
261
261
 
262
262
  if (item_id == 4) {
263
- // 金苹果 - 75 金币
263
+ // Golden apple - 75 coins
264
264
  if (coins >= 75) {
265
265
  scoreboard_add(player, "zs_coins", -75);
266
266
  give(player, "minecraft:golden_apple", 2);
267
- tell(player, "§a购买成功:金苹果 x2");
267
+ tell(player, "§aPurchased: Golden Apple x2");
268
268
  } else {
269
- tell(player, "§c金币不足!需要 75");
269
+ tell(player, "§cNot enough coins! Need 75");
270
270
  }
271
271
  }
272
272
  }
273
273
 
274
- // ===== 结束 =====
274
+ // ===== End =====
275
275
  fn victory() {
276
276
  state.running = 0;
277
277
 
278
- title(@a, "§6胜利!");
279
- subtitle(@a, "§a你们生存了 10 波!");
280
- announce("§4[僵尸生存] §6恭喜!完成全部 10 波!");
278
+ title(@a, "§6Victory!");
279
+ subtitle(@a, "§aYou survived 10 waves!");
280
+ announce("§4[Zombie Survival] §6Congratulations! Completed all 10 waves!");
281
281
 
282
282
  foreach (p in @a) {
283
283
  totem_effect(p);
@@ -290,11 +290,11 @@ fn victory() {
290
290
  fn game_over() {
291
291
  state.running = 0;
292
292
 
293
- title(@a, "§c游戏结束");
294
- subtitle(@a, "§7生存了 " + state.wave + " ");
295
- announce("§4[僵尸生存] §c全员阵亡!最高波数:" + state.wave);
293
+ title(@a, "§cGame Over");
294
+ subtitle(@a, "§7Survived " + state.wave + " waves");
295
+ announce("§4[Zombie Survival] §cAll players eliminated! Highest wave: " + state.wave);
296
296
 
297
- // 清理僵尸
297
+ // Clean up zombies
298
298
  kill(@e[type=zombie]);
299
299
  kill(@e[type=husk]);
300
300
 
@@ -307,8 +307,8 @@ fn update_scoreboard() {
307
307
  }
308
308
 
309
309
  fn count_entities(sel: selector) -> int {
310
- // 使用 execute store 计算实体数量
310
+ // Use execute store to count entities
311
311
  let count: int = 0;
312
- // 实际实现需要 execute store
312
+ // Actual implementation requires execute store
313
313
  return count;
314
314
  }
package/src/index.ts CHANGED
@@ -68,7 +68,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
68
68
 
69
69
  // Parsing
70
70
  const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
71
- const dceResult = shouldRunDce ? eliminateDeadCode(parsedAst) : { program: parsedAst, warnings: [] }
71
+ const dceResult = shouldRunDce ? eliminateDeadCode(parsedAst, preprocessed.ranges) : { program: parsedAst, warnings: [] }
72
72
  const ast = dceResult.program
73
73
 
74
74
  // Type checking (warn mode - collect errors but don't block)
@@ -72,6 +72,7 @@ export interface DCEWarning {
72
72
  code: string
73
73
  line?: number
74
74
  col?: number
75
+ filePath?: string
75
76
  }
76
77
 
77
78
  export class DeadCodeEliminator {
@@ -639,8 +640,23 @@ export class DeadCodeEliminator {
639
640
  }
640
641
  }
641
642
 
642
- export function eliminateDeadCode(program: Program): { program: Program; warnings: DCEWarning[] } {
643
+ export function eliminateDeadCode(
644
+ program: Program,
645
+ sourceRanges?: import('../compile').SourceRange[]
646
+ ): { program: Program; warnings: DCEWarning[] } {
643
647
  const eliminator = new DeadCodeEliminator()
644
648
  const result = eliminator.eliminate(program)
645
- return { program: result, warnings: eliminator.warnings }
649
+ let warnings = eliminator.warnings
650
+
651
+ // Resolve combined-source line numbers back to original file + line
652
+ if (sourceRanges && sourceRanges.length > 0) {
653
+ const { resolveSourceLine } = require('../compile') as typeof import('../compile')
654
+ warnings = warnings.map(w => {
655
+ if (w.line == null) return w
656
+ const resolved = resolveSourceLine(w.line, sourceRanges)
657
+ return { ...w, line: resolved.line, filePath: resolved.filePath ?? w.filePath }
658
+ })
659
+ }
660
+
661
+ return { program: result, warnings }
646
662
  }