redscript-mc 1.2.20 → 1.2.24

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.
Files changed (136) hide show
  1. package/.github/workflows/publish-extension-on-ci.yml +99 -0
  2. package/dist/__tests__/compile-all.test.js +5 -0
  3. package/dist/__tests__/entity-types.test.d.ts +1 -0
  4. package/dist/__tests__/entity-types.test.js +203 -0
  5. package/dist/__tests__/var-allocator.test.d.ts +1 -0
  6. package/dist/__tests__/var-allocator.test.js +69 -0
  7. package/dist/ast/types.d.ts +2 -1
  8. package/dist/cli.js +24 -7
  9. package/dist/codegen/mcfunction/index.d.ts +2 -0
  10. package/dist/codegen/mcfunction/index.js +47 -43
  11. package/dist/codegen/structure/index.d.ts +4 -1
  12. package/dist/codegen/structure/index.js +8 -12
  13. package/dist/codegen/var-allocator.d.ts +28 -0
  14. package/dist/codegen/var-allocator.js +74 -0
  15. package/dist/compile.d.ts +8 -0
  16. package/dist/compile.js +14 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +9 -7
  19. package/dist/lowering/index.d.ts +2 -0
  20. package/dist/lowering/index.js +62 -3
  21. package/dist/optimizer/dce.d.ts +2 -1
  22. package/dist/optimizer/dce.js +13 -2
  23. package/dist/parser/index.js +22 -1
  24. package/dist/typechecker/index.js +30 -0
  25. package/dist/types/entity-hierarchy.d.ts +29 -0
  26. package/dist/types/entity-hierarchy.js +107 -0
  27. package/editors/vscode/out/extension.js +29 -4
  28. package/editors/vscode/package-lock.json +6 -4
  29. package/editors/vscode/package.json +3 -3
  30. package/package.json +1 -1
  31. package/src/__tests__/compile-all.test.ts +6 -0
  32. package/src/__tests__/entity-types.test.ts +236 -0
  33. package/src/__tests__/var-allocator.test.ts +75 -0
  34. package/src/ast/types.ts +8 -4
  35. package/src/cli.ts +28 -8
  36. package/src/codegen/mcfunction/index.ts +55 -48
  37. package/src/codegen/structure/index.ts +9 -14
  38. package/src/codegen/var-allocator.ts +71 -0
  39. package/src/compile.ts +18 -0
  40. package/src/examples/capture_the_flag.mcrs +34 -34
  41. package/src/examples/hunger_games.mcrs +60 -60
  42. package/src/examples/new_features_demo.mcrs +32 -32
  43. package/src/examples/parkour_race.mcrs +58 -58
  44. package/src/examples/zombie_survival.mcrs +73 -73
  45. package/src/index.ts +11 -7
  46. package/src/lowering/index.ts +73 -8
  47. package/src/optimizer/dce.ts +18 -2
  48. package/src/parser/index.ts +20 -1
  49. package/src/typechecker/index.ts +30 -0
  50. package/src/types/entity-hierarchy.ts +120 -0
  51. package/dist/data/arena/function/__load.mcfunction +0 -6
  52. package/dist/data/arena/function/__tick.mcfunction +0 -2
  53. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +0 -3
  54. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +0 -1
  55. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +0 -3
  56. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +0 -7
  57. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +0 -1
  58. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +0 -4
  59. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +0 -6
  60. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +0 -1
  61. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +0 -4
  62. package/dist/data/arena/function/announce_leaders.mcfunction +0 -6
  63. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +0 -1
  64. package/dist/data/arena/function/arena_tick/then_0.mcfunction +0 -4
  65. package/dist/data/arena/function/arena_tick.mcfunction +0 -11
  66. package/dist/data/counter/function/__load.mcfunction +0 -5
  67. package/dist/data/counter/function/__tick.mcfunction +0 -2
  68. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +0 -1
  69. package/dist/data/counter/function/counter_tick/then_0.mcfunction +0 -3
  70. package/dist/data/counter/function/counter_tick.mcfunction +0 -11
  71. package/dist/data/minecraft/tags/function/load.json +0 -5
  72. package/dist/data/minecraft/tags/function/tick.json +0 -5
  73. package/dist/data/quiz/function/__load.mcfunction +0 -16
  74. package/dist/data/quiz/function/__tick.mcfunction +0 -6
  75. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +0 -4
  76. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +0 -4
  77. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +0 -4
  78. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +0 -4
  79. package/dist/data/quiz/function/answer_a.mcfunction +0 -4
  80. package/dist/data/quiz/function/answer_b.mcfunction +0 -4
  81. package/dist/data/quiz/function/answer_c.mcfunction +0 -4
  82. package/dist/data/quiz/function/ask_question/else_1.mcfunction +0 -5
  83. package/dist/data/quiz/function/ask_question/else_4.mcfunction +0 -5
  84. package/dist/data/quiz/function/ask_question/else_7.mcfunction +0 -4
  85. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +0 -1
  86. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +0 -2
  87. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +0 -2
  88. package/dist/data/quiz/function/ask_question/then_0.mcfunction +0 -4
  89. package/dist/data/quiz/function/ask_question/then_3.mcfunction +0 -4
  90. package/dist/data/quiz/function/ask_question/then_6.mcfunction +0 -4
  91. package/dist/data/quiz/function/ask_question.mcfunction +0 -7
  92. package/dist/data/quiz/function/finish_quiz.mcfunction +0 -6
  93. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +0 -5
  94. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +0 -3
  95. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +0 -3
  96. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +0 -3
  97. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +0 -5
  98. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +0 -2
  99. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +0 -2
  100. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +0 -2
  101. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +0 -8
  102. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +0 -2
  103. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +0 -2
  104. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +0 -5
  105. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +0 -5
  106. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +0 -6
  107. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +0 -6
  108. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +0 -5
  109. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +0 -6
  110. package/dist/data/quiz/function/handle_answer.mcfunction +0 -11
  111. package/dist/data/quiz/function/start_quiz.mcfunction +0 -5
  112. package/dist/data/shop/function/__load.mcfunction +0 -7
  113. package/dist/data/shop/function/__tick.mcfunction +0 -3
  114. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +0 -4
  115. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +0 -5
  116. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +0 -5
  117. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +0 -3
  118. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +0 -2
  119. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +0 -2
  120. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +0 -2
  121. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +0 -4
  122. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +0 -4
  123. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +0 -4
  124. package/dist/data/shop/function/complete_purchase.mcfunction +0 -7
  125. package/dist/data/shop/function/handle_shop_trigger.mcfunction +0 -3
  126. package/dist/data/turret/function/__load.mcfunction +0 -5
  127. package/dist/data/turret/function/__tick.mcfunction +0 -4
  128. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +0 -4
  129. package/dist/data/turret/function/deploy_turret.mcfunction +0 -8
  130. package/dist/data/turret/function/turret_tick/at_1.mcfunction +0 -2
  131. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +0 -2
  132. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +0 -2
  133. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +0 -3
  134. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +0 -1
  135. package/dist/data/turret/function/turret_tick.mcfunction +0 -5
  136. package/dist/pack.mcmeta +0 -6
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export interface CompileOptions {
35
35
  typeCheck?: boolean
36
36
  filePath?: string
37
37
  dce?: boolean
38
+ mangle?: boolean
38
39
  }
39
40
 
40
41
  export interface CompileResult {
@@ -45,6 +46,7 @@ export interface CompileResult {
45
46
  typeErrors?: DiagnosticError[]
46
47
  warnings?: Warning[]
47
48
  stats?: OptimizationStats
49
+ sourceMap?: Record<string, string>
48
50
  }
49
51
 
50
52
  /**
@@ -59,6 +61,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
59
61
  const shouldOptimize = options.optimize ?? true
60
62
  const shouldTypeCheck = options.typeCheck ?? true
61
63
  const shouldRunDce = options.dce ?? shouldOptimize
64
+ const mangle = options.mangle ?? false
62
65
  const filePath = options.filePath
63
66
  const preprocessed = preprocessSourceWithMetadata(source, { filePath })
64
67
  const preprocessedSource = preprocessed.source
@@ -68,7 +71,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
68
71
 
69
72
  // Parsing
70
73
  const parsedAst = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
71
- const dceResult = shouldRunDce ? eliminateDeadCode(parsedAst) : { program: parsedAst, warnings: [] }
74
+ const dceResult = shouldRunDce ? eliminateDeadCode(parsedAst, preprocessed.ranges) : { program: parsedAst, warnings: [] }
72
75
  const ast = dceResult.program
73
76
 
74
77
  // Type checking (warn mode - collect errors but don't block)
@@ -83,7 +86,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
83
86
  const ir = lowering.lower(ast)
84
87
 
85
88
  let optimizedIR: IRModule = ir
86
- let generated = generateDatapackWithStats(ir, { optimizeCommands: shouldOptimize })
89
+ let generated = generateDatapackWithStats(ir, { optimizeCommands: shouldOptimize, mangle })
87
90
  let optimizationStats: OptimizationStats | undefined
88
91
 
89
92
  if (shouldOptimize) {
@@ -105,10 +108,10 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
105
108
  const copyPropagatedIR: IRModule = { ...ir, functions: copyPropagatedFunctions }
106
109
  optimizedIR = { ...ir, functions: deadCodeEliminatedFunctions }
107
110
 
108
- const baselineGenerated = generateDatapackWithStats(ir, { optimizeCommands: false })
109
- const beforeDceGenerated = generateDatapackWithStats(copyPropagatedIR, { optimizeCommands: false })
110
- const afterDceGenerated = generateDatapackWithStats(optimizedIR, { optimizeCommands: false })
111
- generated = generateDatapackWithStats(optimizedIR, { optimizeCommands: true })
111
+ const baselineGenerated = generateDatapackWithStats(ir, { optimizeCommands: false, mangle })
112
+ const beforeDceGenerated = generateDatapackWithStats(copyPropagatedIR, { optimizeCommands: false, mangle })
113
+ const afterDceGenerated = generateDatapackWithStats(optimizedIR, { optimizeCommands: false, mangle })
114
+ generated = generateDatapackWithStats(optimizedIR, { optimizeCommands: true, mangle })
112
115
 
113
116
  stats.deadCodeRemoved =
114
117
  countMcfunctionCommands(beforeDceGenerated.files) - countMcfunctionCommands(afterDceGenerated.files)
@@ -124,7 +127,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
124
127
  optimizationStats = stats
125
128
  } else {
126
129
  optimizedIR = ir
127
- generated = generateDatapackWithStats(ir, { optimizeCommands: false })
130
+ generated = generateDatapackWithStats(ir, { optimizeCommands: false, mangle })
128
131
  }
129
132
 
130
133
  return {
@@ -135,6 +138,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
135
138
  typeErrors,
136
139
  warnings: [...dceResult.warnings, ...lowering.warnings],
137
140
  stats: optimizationStats,
141
+ sourceMap: generated.sourceMap,
138
142
  }
139
143
  }
140
144
 
@@ -17,6 +17,7 @@ import type {
17
17
  import type { GlobalVar } from '../ir/types'
18
18
  import * as path from 'path'
19
19
  import { EVENT_TYPES, getEventParamSpecs, isEventTypeName } from '../events/types'
20
+ import { getBaseSelectorType, areCompatibleTypes, getConcreteSubtypes } from '../types/entity-hierarchy'
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // Macro-aware builtins (MC 1.20.2+)
@@ -134,11 +135,21 @@ const ENTITY_TO_MC_TYPE: Partial<Record<EntityTypeName, string>> = {
134
135
  Creeper: 'minecraft:creeper',
135
136
  Spider: 'minecraft:spider',
136
137
  Enderman: 'minecraft:enderman',
138
+ Blaze: 'minecraft:blaze',
139
+ Witch: 'minecraft:witch',
140
+ Slime: 'minecraft:slime',
141
+ ZombieVillager: 'minecraft:zombie_villager',
142
+ Husk: 'minecraft:husk',
143
+ Drowned: 'minecraft:drowned',
144
+ Stray: 'minecraft:stray',
145
+ WitherSkeleton: 'minecraft:wither_skeleton',
146
+ CaveSpider: 'minecraft:cave_spider',
137
147
  Pig: 'minecraft:pig',
138
148
  Cow: 'minecraft:cow',
139
149
  Sheep: 'minecraft:sheep',
140
150
  Chicken: 'minecraft:chicken',
141
151
  Villager: 'minecraft:villager',
152
+ WanderingTrader: 'minecraft:wandering_trader',
142
153
  ArmorStand: 'minecraft:armor_stand',
143
154
  Item: 'minecraft:item',
144
155
  Arrow: 'minecraft:arrow',
@@ -211,6 +222,15 @@ export class Lowering {
211
222
  private intervalCounter: number = 0
212
223
  readonly warnings: Warning[] = []
213
224
 
225
+ // Entity type context stack for W_IMPOSSIBLE_AS warnings
226
+ private entityContextStack: string[] = []
227
+
228
+ private currentEntityContext(): string {
229
+ return this.entityContextStack.length > 0
230
+ ? this.entityContextStack[this.entityContextStack.length - 1]
231
+ : 'Entity'
232
+ }
233
+
214
234
  // Builder state for current function
215
235
  private builder!: LoweringBuilder
216
236
  private varMap: Map<string, string> = new Map()
@@ -1056,17 +1076,31 @@ export class Lowering {
1056
1076
  }
1057
1077
 
1058
1078
  const mcType = ENTITY_TO_MC_TYPE[cond.entityType]
1079
+ const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`
1080
+
1059
1081
  if (!mcType) {
1060
- throw new DiagnosticError(
1061
- 'LoweringError',
1062
- `Cannot lower entity type check for '${cond.entityType}'`,
1063
- cond.span ?? stmt.span ?? { line: 0, col: 0 }
1064
- )
1082
+ // Abstract type — check all concrete subtypes
1083
+ const subtypes = getConcreteSubtypes(cond.entityType)
1084
+ if (subtypes.length === 0) {
1085
+ throw new DiagnosticError(
1086
+ 'LoweringError',
1087
+ `Cannot lower entity type check for '${cond.entityType}'`,
1088
+ cond.span ?? stmt.span ?? { line: 0, col: 0 }
1089
+ )
1090
+ }
1091
+ // Use a temp scoreboard variable to OR multiple type checks
1092
+ this.builder.emitRaw(`scoreboard players set __is_result rs:temp 0`)
1093
+ for (const subtype of subtypes) {
1094
+ if (subtype.mcId) {
1095
+ this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, subtype.mcId)} run scoreboard players set __is_result rs:temp 1`)
1096
+ }
1097
+ }
1098
+ this.builder.emitRaw(`execute if score __is_result rs:temp matches 1 run function ${this.namespace}:${thenFnName}`)
1099
+ } else {
1100
+ // Concrete type — single check
1101
+ this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`)
1065
1102
  }
1066
1103
 
1067
- const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`
1068
- this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`)
1069
-
1070
1104
  const savedBuilder = this.builder
1071
1105
  const savedVarMap = new Map(this.varMap)
1072
1106
  const savedBlockPosVars = new Map(this.blockPosVars)
@@ -1243,12 +1277,22 @@ export class Lowering {
1243
1277
  // In foreach body, the binding maps to @s
1244
1278
  this.varMap.set(stmt.binding, '@s')
1245
1279
 
1280
+ // Track entity context for type narrowing
1281
+ const selectorEntityType = getBaseSelectorType(selector)
1282
+ if (selectorEntityType) {
1283
+ this.entityContextStack.push(selectorEntityType)
1284
+ }
1285
+
1246
1286
  this.builder.startBlock('entry')
1247
1287
  this.lowerBlock(stmt.body)
1248
1288
  if (!this.builder.isBlockSealed()) {
1249
1289
  this.builder.emitReturn()
1250
1290
  }
1251
1291
 
1292
+ if (selectorEntityType) {
1293
+ this.entityContextStack.pop()
1294
+ }
1295
+
1252
1296
  const subFn = this.builder.build(subFnName, [], false)
1253
1297
  this.functions.push(subFn)
1254
1298
 
@@ -1397,6 +1441,18 @@ export class Lowering {
1397
1441
  const selector = this.selectorToString(stmt.selector)
1398
1442
  const subFnName = `${this.currentFn}/as_${this.foreachCounter++}`
1399
1443
 
1444
+ // Check for impossible type assertions (W_IMPOSSIBLE_AS)
1445
+ const innerType = getBaseSelectorType(selector)
1446
+ const outerType = this.currentEntityContext()
1447
+ if (innerType && outerType !== 'Entity' && innerType !== 'Entity' && !areCompatibleTypes(outerType, innerType)) {
1448
+ this.warnings.push({
1449
+ message: `Impossible type assertion: @s is ${outerType} but as-block targets ${innerType}`,
1450
+ code: 'W_IMPOSSIBLE_AS',
1451
+ line: stmt.span?.line,
1452
+ col: stmt.span?.col,
1453
+ })
1454
+ }
1455
+
1400
1456
  this.builder.emitRaw(`execute as ${selector} run function ${this.namespace}:${subFnName}`)
1401
1457
 
1402
1458
  // Create sub-function
@@ -1408,12 +1464,21 @@ export class Lowering {
1408
1464
  this.varMap = new Map(savedVarMap)
1409
1465
  this.blockPosVars = new Map(savedBlockPosVars)
1410
1466
 
1467
+ // Track entity context inside as-block
1468
+ if (innerType) {
1469
+ this.entityContextStack.push(innerType)
1470
+ }
1471
+
1411
1472
  this.builder.startBlock('entry')
1412
1473
  this.lowerBlock(stmt.body)
1413
1474
  if (!this.builder.isBlockSealed()) {
1414
1475
  this.builder.emitReturn()
1415
1476
  }
1416
1477
 
1478
+ if (innerType) {
1479
+ this.entityContextStack.pop()
1480
+ }
1481
+
1417
1482
  const subFn = this.builder.build(subFnName, [], false)
1418
1483
  this.functions.push(subFn)
1419
1484
 
@@ -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
  }
@@ -41,11 +41,21 @@ const ENTITY_TYPE_NAMES = new Set<EntityTypeName>([
41
41
  'Creeper',
42
42
  'Spider',
43
43
  'Enderman',
44
+ 'Blaze',
45
+ 'Witch',
46
+ 'Slime',
47
+ 'ZombieVillager',
48
+ 'Husk',
49
+ 'Drowned',
50
+ 'Stray',
51
+ 'WitherSkeleton',
52
+ 'CaveSpider',
44
53
  'Pig',
45
54
  'Cow',
46
55
  'Sheep',
47
56
  'Chicken',
48
57
  'Villager',
58
+ 'WanderingTrader',
49
59
  'ArmorStand',
50
60
  'Item',
51
61
  'Arrow',
@@ -446,7 +456,16 @@ export class Parser {
446
456
  type = { kind: 'named', name: token.kind }
447
457
  } else if (token.kind === 'ident') {
448
458
  this.advance()
449
- type = { kind: 'struct', name: token.value }
459
+ if (token.value === 'selector' && this.check('<')) {
460
+ this.advance() // consume <
461
+ const entityType = this.expect('ident').value
462
+ this.expect('>')
463
+ type = { kind: 'selector', entityType }
464
+ } else if (token.value === 'selector') {
465
+ type = { kind: 'selector' }
466
+ } else {
467
+ type = { kind: 'struct', name: token.value }
468
+ }
450
469
  } else {
451
470
  this.error(`Expected type, got '${token.kind}'`)
452
471
  }
@@ -31,11 +31,21 @@ const ENTITY_HIERARCHY: Record<EntityTypeName, EntityTypeName | null> = {
31
31
  'Creeper': 'HostileMob',
32
32
  'Spider': 'HostileMob',
33
33
  'Enderman': 'HostileMob',
34
+ 'Blaze': 'HostileMob',
35
+ 'Witch': 'HostileMob',
36
+ 'Slime': 'HostileMob',
37
+ 'ZombieVillager': 'HostileMob',
38
+ 'Husk': 'HostileMob',
39
+ 'Drowned': 'HostileMob',
40
+ 'Stray': 'HostileMob',
41
+ 'WitherSkeleton': 'HostileMob',
42
+ 'CaveSpider': 'HostileMob',
34
43
  'Pig': 'PassiveMob',
35
44
  'Cow': 'PassiveMob',
36
45
  'Sheep': 'PassiveMob',
37
46
  'Chicken': 'PassiveMob',
38
47
  'Villager': 'PassiveMob',
48
+ 'WanderingTrader': 'PassiveMob',
39
49
  'ArmorStand': 'entity',
40
50
  'Item': 'entity',
41
51
  'Arrow': 'entity',
@@ -53,6 +63,24 @@ const MC_TYPE_TO_ENTITY: Record<string, EntityTypeName> = {
53
63
  'minecraft:spider': 'Spider',
54
64
  'enderman': 'Enderman',
55
65
  'minecraft:enderman': 'Enderman',
66
+ 'blaze': 'Blaze',
67
+ 'minecraft:blaze': 'Blaze',
68
+ 'witch': 'Witch',
69
+ 'minecraft:witch': 'Witch',
70
+ 'slime': 'Slime',
71
+ 'minecraft:slime': 'Slime',
72
+ 'zombie_villager': 'ZombieVillager',
73
+ 'minecraft:zombie_villager': 'ZombieVillager',
74
+ 'husk': 'Husk',
75
+ 'minecraft:husk': 'Husk',
76
+ 'drowned': 'Drowned',
77
+ 'minecraft:drowned': 'Drowned',
78
+ 'stray': 'Stray',
79
+ 'minecraft:stray': 'Stray',
80
+ 'wither_skeleton': 'WitherSkeleton',
81
+ 'minecraft:wither_skeleton': 'WitherSkeleton',
82
+ 'cave_spider': 'CaveSpider',
83
+ 'minecraft:cave_spider': 'CaveSpider',
56
84
  'pig': 'Pig',
57
85
  'minecraft:pig': 'Pig',
58
86
  'cow': 'Cow',
@@ -63,6 +91,8 @@ const MC_TYPE_TO_ENTITY: Record<string, EntityTypeName> = {
63
91
  'minecraft:chicken': 'Chicken',
64
92
  'villager': 'Villager',
65
93
  'minecraft:villager': 'Villager',
94
+ 'wandering_trader': 'WanderingTrader',
95
+ 'minecraft:wandering_trader': 'WanderingTrader',
66
96
  'armor_stand': 'ArmorStand',
67
97
  'minecraft:armor_stand': 'ArmorStand',
68
98
  'item': 'Item',
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Entity Type Hierarchy
3
+ *
4
+ * Closed inheritance tree mapping to Minecraft's entity registry.
5
+ * Used for type narrowing (is checks), selector<T> annotations,
6
+ * and W_IMPOSSIBLE_AS warnings.
7
+ */
8
+
9
+ export interface EntityTypeNode {
10
+ name: string
11
+ mcId: string | null // null for abstract types
12
+ abstract: boolean
13
+ parent: string | null // null for root "Entity"
14
+ }
15
+
16
+ export const ENTITY_TYPES: EntityTypeNode[] = [
17
+ // Root
18
+ { name: 'Entity', mcId: null, abstract: true, parent: null },
19
+
20
+ // Direct children of Entity
21
+ { name: 'Player', mcId: 'minecraft:player', abstract: false, parent: 'Entity' },
22
+ { name: 'ArmorStand', mcId: 'minecraft:armor_stand', abstract: false, parent: 'Entity' },
23
+ { name: 'Item', mcId: 'minecraft:item', abstract: false, parent: 'Entity' },
24
+ { name: 'Arrow', mcId: 'minecraft:arrow', abstract: false, parent: 'Entity' },
25
+
26
+ // Mob hierarchy
27
+ { name: 'Mob', mcId: null, abstract: true, parent: 'Entity' },
28
+
29
+ // Hostile mobs
30
+ { name: 'HostileMob', mcId: null, abstract: true, parent: 'Mob' },
31
+ { name: 'Zombie', mcId: 'minecraft:zombie', abstract: false, parent: 'HostileMob' },
32
+ { name: 'Skeleton', mcId: 'minecraft:skeleton', abstract: false, parent: 'HostileMob' },
33
+ { name: 'Creeper', mcId: 'minecraft:creeper', abstract: false, parent: 'HostileMob' },
34
+ { name: 'Spider', mcId: 'minecraft:spider', abstract: false, parent: 'HostileMob' },
35
+ { name: 'Enderman', mcId: 'minecraft:enderman', abstract: false, parent: 'HostileMob' },
36
+ { name: 'Blaze', mcId: 'minecraft:blaze', abstract: false, parent: 'HostileMob' },
37
+ { name: 'Witch', mcId: 'minecraft:witch', abstract: false, parent: 'HostileMob' },
38
+ { name: 'Slime', mcId: 'minecraft:slime', abstract: false, parent: 'HostileMob' },
39
+ { name: 'ZombieVillager', mcId: 'minecraft:zombie_villager', abstract: false, parent: 'HostileMob' },
40
+ { name: 'Husk', mcId: 'minecraft:husk', abstract: false, parent: 'HostileMob' },
41
+ { name: 'Drowned', mcId: 'minecraft:drowned', abstract: false, parent: 'HostileMob' },
42
+ { name: 'Stray', mcId: 'minecraft:stray', abstract: false, parent: 'HostileMob' },
43
+ { name: 'WitherSkeleton', mcId: 'minecraft:wither_skeleton', abstract: false, parent: 'HostileMob' },
44
+ { name: 'CaveSpider', mcId: 'minecraft:cave_spider', abstract: false, parent: 'HostileMob' },
45
+
46
+ // Passive mobs
47
+ { name: 'PassiveMob', mcId: null, abstract: true, parent: 'Mob' },
48
+ { name: 'Pig', mcId: 'minecraft:pig', abstract: false, parent: 'PassiveMob' },
49
+ { name: 'Cow', mcId: 'minecraft:cow', abstract: false, parent: 'PassiveMob' },
50
+ { name: 'Sheep', mcId: 'minecraft:sheep', abstract: false, parent: 'PassiveMob' },
51
+ { name: 'Chicken', mcId: 'minecraft:chicken', abstract: false, parent: 'PassiveMob' },
52
+ { name: 'Villager', mcId: 'minecraft:villager', abstract: false, parent: 'PassiveMob' },
53
+ { name: 'WanderingTrader', mcId: 'minecraft:wandering_trader', abstract: false, parent: 'PassiveMob' },
54
+ ]
55
+
56
+ /** Map from lowercase name → EntityTypeNode */
57
+ export const ENTITY_TYPE_MAP: Map<string, EntityTypeNode> = new Map(
58
+ ENTITY_TYPES.map(t => [t.name.toLowerCase(), t])
59
+ )
60
+
61
+ /** Map from mcId (without namespace) → EntityTypeNode */
62
+ export const ENTITY_TYPE_BY_MCID: Map<string, EntityTypeNode> = new Map(
63
+ ENTITY_TYPES
64
+ .filter(t => t.mcId !== null)
65
+ .map(t => [t.mcId!.replace('minecraft:', ''), t])
66
+ )
67
+
68
+ /** Check if typeA is a subtype of typeB (recursive ancestry) */
69
+ export function isSubtype(typeA: string, typeB: string): boolean {
70
+ if (typeA === typeB) return true
71
+ const node = ENTITY_TYPE_MAP.get(typeA.toLowerCase())
72
+ if (!node || !node.parent) return false
73
+ return isSubtype(node.parent, typeB)
74
+ }
75
+
76
+ /** True if one type is a subtype of the other (in either direction) */
77
+ export function areCompatibleTypes(outerType: string, innerType: string): boolean {
78
+ return isSubtype(outerType, innerType) || isSubtype(innerType, outerType)
79
+ }
80
+
81
+ /** Get all non-abstract leaf types under a given node */
82
+ export function getConcreteSubtypes(typeName: string): EntityTypeNode[] {
83
+ const results: EntityTypeNode[] = []
84
+ for (const node of ENTITY_TYPES) {
85
+ if (!node.abstract && isSubtype(node.name, typeName)) {
86
+ results.push(node)
87
+ }
88
+ }
89
+ return results
90
+ }
91
+
92
+ /** Parse "type=zombie" from a selector string, return the entity type name or null */
93
+ export function getSelectorEntityType(selector: string): string | null {
94
+ const match = selector.match(/type=(?:minecraft:)?([a-z_]+)/)
95
+ if (!match) return null
96
+ const mcId = match[1]
97
+ const node = ENTITY_TYPE_BY_MCID.get(mcId)
98
+ return node ? node.name : null
99
+ }
100
+
101
+ /** Determine the base entity type from a selector kind:
102
+ * @a/@p/@r → "Player", @e → "Entity" or from type filter, @s → null */
103
+ export function getBaseSelectorType(selector: string): string | null {
104
+ const trimmed = selector.trim()
105
+
106
+ // Check for type filter first (works for any selector)
107
+ const typeFromFilter = getSelectorEntityType(trimmed)
108
+
109
+ if (trimmed.startsWith('@a') || trimmed.startsWith('@p') || trimmed.startsWith('@r')) {
110
+ return typeFromFilter ?? 'Player'
111
+ }
112
+ if (trimmed.startsWith('@e')) {
113
+ return typeFromFilter ?? 'Entity'
114
+ }
115
+ // @s — context-dependent, we don't know unless there's a type filter
116
+ if (trimmed.startsWith('@s')) {
117
+ return typeFromFilter ?? null
118
+ }
119
+ return null
120
+ }
@@ -1,6 +0,0 @@
1
- # RedScript runtime init
2
- scoreboard objectives add rs dummy
3
- scoreboard players set $const_1 rs 1
4
- scoreboard players set $const_200 rs 200
5
- scoreboard players set $const_0 rs 0
6
- scoreboard players set $const_0 rs 0
@@ -1,2 +0,0 @@
1
- # RedScript tick dispatcher
2
- function arena:arena_tick
@@ -1,3 +0,0 @@
1
- # block: else_1
2
- say Arena update: no PvP kills yet.
3
- function arena:announce_leaders/merge_2
@@ -1,3 +0,0 @@
1
- # block: then_0
2
- scoreboard players operation $top_kills rs = $kills rs
3
- function arena:announce_leaders/foreach_0/merge_2
@@ -1,7 +0,0 @@
1
- # block: entry
2
- execute store result score $t0 rs run scoreboard players get @s kills
3
- scoreboard players operation $kills rs = $t0 rs
4
- scoreboard players set $t1 rs 0
5
- execute if score $t0 rs > $top_kills rs run scoreboard players set $t1 rs 1
6
- execute if score $t1 rs matches 1.. run function arena:announce_leaders/foreach_0/then_0
7
- execute if score $t1 rs matches ..0 run function arena:announce_leaders/foreach_0/merge_2
@@ -1,4 +0,0 @@
1
- # block: then_0
2
- tellraw @s {"text":"You are leading the arena right now."}
3
- title @s title {"text":"Arena Leader"}
4
- function arena:announce_leaders/foreach_1/merge_2
@@ -1,6 +0,0 @@
1
- # block: entry
2
- execute store result score $t0 rs run scoreboard players get @s kills
3
- scoreboard players set $t1 rs 0
4
- execute if score $t0 rs = $top_kills rs run scoreboard players set $t1 rs 1
5
- execute if score $t1 rs matches 1.. run function arena:announce_leaders/foreach_1/then_0
6
- execute if score $t1 rs matches ..0 run function arena:announce_leaders/foreach_1/merge_2
@@ -1 +0,0 @@
1
- # block: merge_2
@@ -1,4 +0,0 @@
1
- # block: then_0
2
- say Arena update: current leader check complete.
3
- execute as @a run function arena:announce_leaders/foreach_1
4
- function arena:announce_leaders/merge_2
@@ -1,6 +0,0 @@
1
- # block: entry
2
- execute as @a run function arena:announce_leaders/foreach_0
3
- scoreboard players set $t0 rs 0
4
- execute if score $const_0 rs > $const_0 rs run scoreboard players set $t0 rs 1
5
- execute if score $t0 rs matches 1.. run function arena:announce_leaders/then_0
6
- execute if score $t0 rs matches ..0 run function arena:announce_leaders/else_1
@@ -1 +0,0 @@
1
- # block: merge_2
@@ -1,4 +0,0 @@
1
- # block: then_0
2
- function arena:announce_leaders
3
- scoreboard players operation $t5 rs = $ret rs
4
- function arena:arena_tick/merge_2
@@ -1,11 +0,0 @@
1
- # block: entry
2
- execute store result score $t0 rs run scoreboard players get arena ticks
3
- scoreboard players operation $t1 rs = $t0 rs
4
- scoreboard players operation $t1 rs += $const_1 rs
5
- execute store result score arena ticks run scoreboard players get $t2 rs
6
- scoreboard players operation $t3 rs = $t1 rs
7
- scoreboard players operation $t3 rs %= $const_200 rs
8
- scoreboard players set $t4 rs 0
9
- execute if score $t3 rs = $const_0 rs run scoreboard players set $t4 rs 1
10
- execute if score $t4 rs matches 1.. run function arena:arena_tick/then_0
11
- execute if score $t4 rs matches ..0 run function arena:arena_tick/merge_2
@@ -1,5 +0,0 @@
1
- # RedScript runtime init
2
- scoreboard objectives add rs dummy
3
- scoreboard players set $const_1 rs 1
4
- scoreboard players set $const_100 rs 100
5
- scoreboard players set $const_0 rs 0
@@ -1,2 +0,0 @@
1
- # RedScript tick dispatcher
2
- function counter:counter_tick
@@ -1 +0,0 @@
1
- # block: merge_2
@@ -1,3 +0,0 @@
1
- # block: then_0
2
- say Counter reached another 100 ticks
3
- function counter:counter_tick/merge_2
@@ -1,11 +0,0 @@
1
- # block: entry
2
- execute store result score $t0 rs run scoreboard players get counter ticks
3
- scoreboard players operation $t1 rs = $t0 rs
4
- scoreboard players operation $t1 rs += $const_1 rs
5
- execute store result score counter ticks run scoreboard players get $t2 rs
6
- scoreboard players operation $t3 rs = $t1 rs
7
- scoreboard players operation $t3 rs %= $const_100 rs
8
- scoreboard players set $t4 rs 0
9
- execute if score $t3 rs = $const_0 rs run scoreboard players set $t4 rs 1
10
- execute if score $t4 rs matches 1.. run function counter:counter_tick/then_0
11
- execute if score $t4 rs matches ..0 run function counter:counter_tick/merge_2
@@ -1,5 +0,0 @@
1
- {
2
- "values": [
3
- "turret:__load"
4
- ]
5
- }
@@ -1,5 +0,0 @@
1
- {
2
- "values": [
3
- "turret:__tick"
4
- ]
5
- }
@@ -1,16 +0,0 @@
1
- # RedScript runtime init
2
- scoreboard objectives add rs dummy
3
- scoreboard objectives add quiz_start trigger
4
- scoreboard players enable @a quiz_start
5
- scoreboard objectives add quiz_a trigger
6
- scoreboard players enable @a quiz_a
7
- scoreboard objectives add quiz_b trigger
8
- scoreboard players enable @a quiz_b
9
- scoreboard objectives add quiz_c trigger
10
- scoreboard players enable @a quiz_c
11
- scoreboard players set $const_1 rs 1
12
- scoreboard players set $const_2 rs 2
13
- scoreboard players set $const_3 rs 3
14
- scoreboard players set $const_1 rs 1
15
- scoreboard players set $const_2 rs 2
16
- scoreboard players set $const_3 rs 3
@@ -1,6 +0,0 @@
1
- # RedScript tick dispatcher
2
- # Trigger checks
3
- execute as @a[scores={quiz_start=1..}] run function quiz:__trigger_quiz_start_dispatch
4
- execute as @a[scores={quiz_a=1..}] run function quiz:__trigger_quiz_a_dispatch
5
- execute as @a[scores={quiz_b=1..}] run function quiz:__trigger_quiz_b_dispatch
6
- execute as @a[scores={quiz_c=1..}] run function quiz:__trigger_quiz_c_dispatch
@@ -1,4 +0,0 @@
1
- # Trigger dispatch for quiz_a
2
- function quiz:answer_a
3
- scoreboard players set @s quiz_a 0
4
- scoreboard players enable @s quiz_a
@@ -1,4 +0,0 @@
1
- # Trigger dispatch for quiz_b
2
- function quiz:answer_b
3
- scoreboard players set @s quiz_b 0
4
- scoreboard players enable @s quiz_b
@@ -1,4 +0,0 @@
1
- # Trigger dispatch for quiz_c
2
- function quiz:answer_c
3
- scoreboard players set @s quiz_c 0
4
- scoreboard players enable @s quiz_c