redscript-mc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
- package/.github/ISSUE_TEMPLATE/wrong_output.md +33 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +34 -0
- package/.github/workflows/ci.yml +29 -0
- package/.github/workflows/publish-extension.yml +35 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/README.zh.md +261 -0
- package/dist/__tests__/cli.test.d.ts +1 -0
- package/dist/__tests__/cli.test.js +140 -0
- package/dist/__tests__/codegen.test.d.ts +1 -0
- package/dist/__tests__/codegen.test.js +121 -0
- package/dist/__tests__/diagnostics.test.d.ts +4 -0
- package/dist/__tests__/diagnostics.test.js +149 -0
- package/dist/__tests__/e2e.test.d.ts +6 -0
- package/dist/__tests__/e2e.test.js +1528 -0
- package/dist/__tests__/lexer.test.d.ts +1 -0
- package/dist/__tests__/lexer.test.js +316 -0
- package/dist/__tests__/lowering.test.d.ts +1 -0
- package/dist/__tests__/lowering.test.js +819 -0
- package/dist/__tests__/mc-integration.test.d.ts +12 -0
- package/dist/__tests__/mc-integration.test.js +395 -0
- package/dist/__tests__/mc-syntax.test.d.ts +1 -0
- package/dist/__tests__/mc-syntax.test.js +112 -0
- package/dist/__tests__/nbt.test.d.ts +1 -0
- package/dist/__tests__/nbt.test.js +82 -0
- package/dist/__tests__/optimizer-advanced.test.d.ts +1 -0
- package/dist/__tests__/optimizer-advanced.test.js +124 -0
- package/dist/__tests__/optimizer.test.d.ts +1 -0
- package/dist/__tests__/optimizer.test.js +118 -0
- package/dist/__tests__/parser.test.d.ts +1 -0
- package/dist/__tests__/parser.test.js +717 -0
- package/dist/__tests__/repl.test.d.ts +1 -0
- package/dist/__tests__/repl.test.js +27 -0
- package/dist/__tests__/runtime.test.d.ts +1 -0
- package/dist/__tests__/runtime.test.js +276 -0
- package/dist/__tests__/structure-optimizer.test.d.ts +1 -0
- package/dist/__tests__/structure-optimizer.test.js +33 -0
- package/dist/__tests__/typechecker.test.d.ts +1 -0
- package/dist/__tests__/typechecker.test.js +364 -0
- package/dist/ast/types.d.ts +357 -0
- package/dist/ast/types.js +9 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +407 -0
- package/dist/codegen/cmdblock/index.d.ts +26 -0
- package/dist/codegen/cmdblock/index.js +45 -0
- package/dist/codegen/mcfunction/index.d.ts +34 -0
- package/dist/codegen/mcfunction/index.js +413 -0
- package/dist/codegen/structure/index.d.ts +18 -0
- package/dist/codegen/structure/index.js +249 -0
- package/dist/compile.d.ts +30 -0
- package/dist/compile.js +152 -0
- package/dist/data/arena/function/__load.mcfunction +6 -0
- package/dist/data/arena/function/__tick.mcfunction +2 -0
- package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
- package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
- package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
- package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
- package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
- package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
- package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
- package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
- package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
- package/dist/data/arena/function/arena_tick.mcfunction +11 -0
- package/dist/data/counter/function/__load.mcfunction +5 -0
- package/dist/data/counter/function/__tick.mcfunction +2 -0
- package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
- package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
- package/dist/data/counter/function/counter_tick.mcfunction +11 -0
- package/dist/data/minecraft/tags/function/load.json +5 -0
- package/dist/data/minecraft/tags/function/tick.json +5 -0
- package/dist/data/quiz/function/__load.mcfunction +16 -0
- package/dist/data/quiz/function/__tick.mcfunction +6 -0
- package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
- package/dist/data/quiz/function/answer_a.mcfunction +4 -0
- package/dist/data/quiz/function/answer_b.mcfunction +4 -0
- package/dist/data/quiz/function/answer_c.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
- package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
- package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
- package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
- package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
- package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
- package/dist/data/quiz/function/ask_question.mcfunction +7 -0
- package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
- package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
- package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
- package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
- package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
- package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
- package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
- package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
- package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
- package/dist/data/shop/function/__load.mcfunction +7 -0
- package/dist/data/shop/function/__tick.mcfunction +3 -0
- package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
- package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
- package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
- package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
- package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
- package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
- package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
- package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
- package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
- package/dist/data/turret/function/__load.mcfunction +5 -0
- package/dist/data/turret/function/__tick.mcfunction +4 -0
- package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
- package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
- package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
- package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
- package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
- package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
- package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
- package/dist/data/turret/function/turret_tick.mcfunction +5 -0
- package/dist/diagnostics/index.d.ts +44 -0
- package/dist/diagnostics/index.js +140 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +126 -0
- package/dist/ir/builder.d.ts +32 -0
- package/dist/ir/builder.js +99 -0
- package/dist/ir/types.d.ts +117 -0
- package/dist/ir/types.js +15 -0
- package/dist/lexer/index.d.ts +36 -0
- package/dist/lexer/index.js +458 -0
- package/dist/lowering/index.d.ts +106 -0
- package/dist/lowering/index.js +2041 -0
- package/dist/mc-test/client.d.ts +128 -0
- package/dist/mc-test/client.js +174 -0
- package/dist/mc-test/runner.d.ts +28 -0
- package/dist/mc-test/runner.js +150 -0
- package/dist/mc-test/setup.d.ts +11 -0
- package/dist/mc-test/setup.js +98 -0
- package/dist/mc-validator/index.d.ts +17 -0
- package/dist/mc-validator/index.js +322 -0
- package/dist/nbt/index.d.ts +86 -0
- package/dist/nbt/index.js +250 -0
- package/dist/optimizer/commands.d.ts +36 -0
- package/dist/optimizer/commands.js +349 -0
- package/dist/optimizer/passes.d.ts +34 -0
- package/dist/optimizer/passes.js +227 -0
- package/dist/optimizer/structure.d.ts +8 -0
- package/dist/optimizer/structure.js +344 -0
- package/dist/pack.mcmeta +6 -0
- package/dist/parser/index.d.ts +76 -0
- package/dist/parser/index.js +1193 -0
- package/dist/repl.d.ts +16 -0
- package/dist/repl.js +165 -0
- package/dist/runtime/index.d.ts +101 -0
- package/dist/runtime/index.js +1288 -0
- package/dist/typechecker/index.d.ts +42 -0
- package/dist/typechecker/index.js +629 -0
- package/docs/COMPILATION_STATS.md +142 -0
- package/docs/IMPLEMENTATION_GUIDE.md +512 -0
- package/docs/LANGUAGE_REFERENCE.md +415 -0
- package/docs/MC_MAPPING.md +280 -0
- package/docs/STRUCTURE_TARGET.md +80 -0
- package/docs/mc-reference/commands.md +259 -0
- package/editors/vscode/.vscodeignore +10 -0
- package/editors/vscode/LICENSE +21 -0
- package/editors/vscode/README.md +78 -0
- package/editors/vscode/build.mjs +28 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/mcfunction-language-configuration.json +28 -0
- package/editors/vscode/out/extension.js +7236 -0
- package/editors/vscode/package-lock.json +566 -0
- package/editors/vscode/package.json +137 -0
- package/editors/vscode/redscript-language-configuration.json +28 -0
- package/editors/vscode/snippets/redscript.json +114 -0
- package/editors/vscode/src/codeactions.ts +89 -0
- package/editors/vscode/src/completion.ts +130 -0
- package/editors/vscode/src/extension.ts +239 -0
- package/editors/vscode/src/hover.ts +1120 -0
- package/editors/vscode/src/symbols.ts +207 -0
- package/editors/vscode/syntaxes/mcfunction.tmLanguage.json +740 -0
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +357 -0
- package/editors/vscode/tsconfig.json +13 -0
- package/jest.config.js +5 -0
- package/package.json +38 -0
- package/src/__tests__/cli.test.ts +130 -0
- package/src/__tests__/codegen.test.ts +128 -0
- package/src/__tests__/diagnostics.test.ts +195 -0
- package/src/__tests__/e2e.test.ts +1721 -0
- package/src/__tests__/fixtures/mc-commands-1.21.4.json +18734 -0
- package/src/__tests__/formatter.test.ts +46 -0
- package/src/__tests__/lexer.test.ts +356 -0
- package/src/__tests__/lowering.test.ts +962 -0
- package/src/__tests__/mc-integration.test.ts +409 -0
- package/src/__tests__/mc-syntax.test.ts +96 -0
- package/src/__tests__/nbt.test.ts +58 -0
- package/src/__tests__/optimizer-advanced.test.ts +144 -0
- package/src/__tests__/optimizer.test.ts +129 -0
- package/src/__tests__/parser.test.ts +800 -0
- package/src/__tests__/repl.test.ts +33 -0
- package/src/__tests__/runtime.test.ts +289 -0
- package/src/__tests__/structure-optimizer.test.ts +38 -0
- package/src/__tests__/typechecker.test.ts +395 -0
- package/src/ast/types.ts +248 -0
- package/src/cli.ts +445 -0
- package/src/codegen/cmdblock/index.ts +63 -0
- package/src/codegen/mcfunction/index.ts +471 -0
- package/src/codegen/structure/index.ts +305 -0
- package/src/compile.ts +188 -0
- package/src/diagnostics/index.ts +186 -0
- package/src/examples/README.md +77 -0
- package/src/examples/SHOWCASE_GAME.md +43 -0
- package/src/examples/arena.rs +44 -0
- package/src/examples/counter.rs +12 -0
- package/src/examples/pvp_arena.rs +131 -0
- package/src/examples/quiz.rs +90 -0
- package/src/examples/rpg.rs +13 -0
- package/src/examples/shop.rs +30 -0
- package/src/examples/showcase_game.rs +552 -0
- package/src/examples/stdlib_demo.rs +181 -0
- package/src/examples/turret.rs +27 -0
- package/src/examples/world_manager.rs +23 -0
- package/src/formatter/index.ts +22 -0
- package/src/index.ts +161 -0
- package/src/ir/builder.ts +114 -0
- package/src/ir/types.ts +119 -0
- package/src/lexer/index.ts +555 -0
- package/src/lowering/index.ts +2406 -0
- package/src/mc-test/client.ts +259 -0
- package/src/mc-test/runner.ts +140 -0
- package/src/mc-test/setup.ts +70 -0
- package/src/mc-validator/index.ts +367 -0
- package/src/nbt/index.ts +321 -0
- package/src/optimizer/commands.ts +416 -0
- package/src/optimizer/passes.ts +233 -0
- package/src/optimizer/structure.ts +441 -0
- package/src/parser/index.ts +1437 -0
- package/src/repl.ts +165 -0
- package/src/runtime/index.ts +1403 -0
- package/src/stdlib/README.md +156 -0
- package/src/stdlib/combat.rs +20 -0
- package/src/stdlib/cooldown.rs +45 -0
- package/src/stdlib/math.rs +49 -0
- package/src/stdlib/mobs.rs +99 -0
- package/src/stdlib/player.rs +29 -0
- package/src/stdlib/strings.rs +7 -0
- package/src/stdlib/timer.rs +51 -0
- package/src/templates/README.md +126 -0
- package/src/templates/combat.rs +96 -0
- package/src/templates/economy.rs +40 -0
- package/src/templates/mini-game-framework.rs +117 -0
- package/src/templates/quest.rs +78 -0
- package/src/test_programs/zombie_game.rs +25 -0
- package/src/typechecker/index.ts +737 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,1403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCRuntime - Minecraft Command Runtime Simulator
|
|
3
|
+
*
|
|
4
|
+
* A TypeScript interpreter that simulates the subset of MC commands that
|
|
5
|
+
* RedScript generates, allowing behavioral testing without a real server.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { compile as rsCompile } from '../compile'
|
|
9
|
+
import * as fs from 'fs'
|
|
10
|
+
import * as path from 'path'
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface Entity {
|
|
17
|
+
id: string
|
|
18
|
+
tags: Set<string>
|
|
19
|
+
scores: Map<string, number>
|
|
20
|
+
selector: string
|
|
21
|
+
type?: string
|
|
22
|
+
position?: { x: number; y: number; z: number }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Range {
|
|
26
|
+
min: number
|
|
27
|
+
max: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SelectorFilters {
|
|
31
|
+
tag?: string[]
|
|
32
|
+
notTag?: string[]
|
|
33
|
+
type?: string[]
|
|
34
|
+
notType?: string[]
|
|
35
|
+
limit?: number
|
|
36
|
+
scores?: Map<string, Range>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Selector & Range Parsing
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function parseRange(s: string): Range {
|
|
44
|
+
if (s.includes('..')) {
|
|
45
|
+
const [left, right] = s.split('..')
|
|
46
|
+
return {
|
|
47
|
+
min: left === '' ? -Infinity : parseInt(left, 10),
|
|
48
|
+
max: right === '' ? Infinity : parseInt(right, 10),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const val = parseInt(s, 10)
|
|
52
|
+
return { min: val, max: val }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function matchesRange(value: number, range: Range): boolean {
|
|
56
|
+
return value >= range.min && value <= range.max
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function canonicalEntityType(entityType: string): string {
|
|
60
|
+
return entityType.includes(':') ? entityType : `minecraft:${entityType}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseFilters(content: string): SelectorFilters {
|
|
64
|
+
const filters: SelectorFilters = {
|
|
65
|
+
tag: [],
|
|
66
|
+
notTag: [],
|
|
67
|
+
type: [],
|
|
68
|
+
notType: [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!content) return filters
|
|
72
|
+
|
|
73
|
+
// Handle scores={...} separately
|
|
74
|
+
let processed = content
|
|
75
|
+
const scoresMatch = content.match(/scores=\{([^}]*)\}/)
|
|
76
|
+
if (scoresMatch) {
|
|
77
|
+
filters.scores = new Map()
|
|
78
|
+
const scoresPart = scoresMatch[1]
|
|
79
|
+
const scoreEntries = scoresPart.split(',')
|
|
80
|
+
for (const entry of scoreEntries) {
|
|
81
|
+
const [obj, range] = entry.split('=')
|
|
82
|
+
if (obj && range) {
|
|
83
|
+
filters.scores.set(obj.trim(), parseRange(range.trim()))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
processed = content.replace(/,?scores=\{[^}]*\},?/, ',').replace(/^,|,$/g, '')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parse remaining filters
|
|
90
|
+
const parts = processed.split(',').filter(p => p.trim())
|
|
91
|
+
for (const part of parts) {
|
|
92
|
+
const [key, value] = part.split('=').map(s => s.trim())
|
|
93
|
+
if (key === 'tag') {
|
|
94
|
+
if (value.startsWith('!')) {
|
|
95
|
+
filters.notTag!.push(value.slice(1))
|
|
96
|
+
} else {
|
|
97
|
+
filters.tag!.push(value)
|
|
98
|
+
}
|
|
99
|
+
} else if (key === 'type') {
|
|
100
|
+
if (value.startsWith('!')) {
|
|
101
|
+
filters.notType!.push(value.slice(1))
|
|
102
|
+
} else {
|
|
103
|
+
filters.type!.push(value)
|
|
104
|
+
}
|
|
105
|
+
} else if (key === 'limit') {
|
|
106
|
+
filters.limit = parseInt(value, 10)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return filters
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function matchesFilters(entity: Entity, filters: SelectorFilters, objective: string = 'rs'): boolean {
|
|
114
|
+
// Check required tags
|
|
115
|
+
for (const tag of filters.tag || []) {
|
|
116
|
+
if (!entity.tags.has(tag)) return false
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check excluded tags
|
|
120
|
+
for (const notTag of filters.notTag || []) {
|
|
121
|
+
if (entity.tags.has(notTag)) return false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check types
|
|
125
|
+
if ((filters.type?.length ?? 0) > 0) {
|
|
126
|
+
const entityType = canonicalEntityType(entity.type ?? 'minecraft:armor_stand')
|
|
127
|
+
const allowedTypes = filters.type!.map(canonicalEntityType)
|
|
128
|
+
if (!allowedTypes.includes(entityType)) {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const notType of filters.notType || []) {
|
|
133
|
+
const entityType = canonicalEntityType(entity.type ?? 'minecraft:armor_stand')
|
|
134
|
+
if (canonicalEntityType(notType) === entityType) {
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check scores
|
|
140
|
+
if (filters.scores) {
|
|
141
|
+
for (const [obj, range] of filters.scores) {
|
|
142
|
+
const score = entity.scores.get(obj) ?? 0
|
|
143
|
+
if (!matchesRange(score, range)) return false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseSelector(
|
|
151
|
+
sel: string,
|
|
152
|
+
entities: Entity[],
|
|
153
|
+
executor?: Entity
|
|
154
|
+
): Entity[] {
|
|
155
|
+
// Handle @s
|
|
156
|
+
if (sel === '@s') {
|
|
157
|
+
return executor ? [executor] : []
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle bare selectors
|
|
161
|
+
if (sel === '@e' || sel === '@a') {
|
|
162
|
+
return [...entities]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Parse selector with brackets
|
|
166
|
+
const match = sel.match(/^(@[eaps])(?:\[(.*)\])?$/)
|
|
167
|
+
if (!match) {
|
|
168
|
+
return []
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const [, selectorType, bracketContent] = match
|
|
172
|
+
|
|
173
|
+
// @s with filters
|
|
174
|
+
if (selectorType === '@s') {
|
|
175
|
+
if (!executor) return []
|
|
176
|
+
const filters = parseFilters(bracketContent || '')
|
|
177
|
+
if (matchesFilters(executor, filters)) {
|
|
178
|
+
return [executor]
|
|
179
|
+
}
|
|
180
|
+
return []
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// @e/@a with filters
|
|
184
|
+
const filters = parseFilters(bracketContent || '')
|
|
185
|
+
let result = entities.filter(e => matchesFilters(e, filters))
|
|
186
|
+
|
|
187
|
+
// Apply limit
|
|
188
|
+
if (filters.limit !== undefined) {
|
|
189
|
+
result = result.slice(0, filters.limit)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// JSON Component Parsing
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// NBT Parsing
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
function parseNBT(nbt: string): Record<string, any> {
|
|
204
|
+
// Simple NBT parser for Tags array
|
|
205
|
+
const result: Record<string, any> = {}
|
|
206
|
+
|
|
207
|
+
const tagsMatch = nbt.match(/Tags:\s*\[(.*?)\]/)
|
|
208
|
+
if (tagsMatch) {
|
|
209
|
+
const tagsStr = tagsMatch[1]
|
|
210
|
+
result.Tags = tagsStr
|
|
211
|
+
.split(',')
|
|
212
|
+
.map(s => s.trim().replace(/^["']|["']$/g, ''))
|
|
213
|
+
.filter(s => s.length > 0)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// MCRuntime Class
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
export class MCRuntime {
|
|
224
|
+
// Scoreboard state: objective → (player → score)
|
|
225
|
+
scoreboard: Map<string, Map<string, number>> = new Map()
|
|
226
|
+
|
|
227
|
+
// NBT storage: "namespace:path" → JSON value
|
|
228
|
+
storage: Map<string, any> = new Map()
|
|
229
|
+
|
|
230
|
+
// Entities in world
|
|
231
|
+
entities: Entity[] = []
|
|
232
|
+
|
|
233
|
+
// Loaded functions: "ns:name" → lines of mcfunction
|
|
234
|
+
functions: Map<string, string[]> = new Map()
|
|
235
|
+
|
|
236
|
+
// Log of say/tellraw/title output
|
|
237
|
+
chatLog: string[] = []
|
|
238
|
+
|
|
239
|
+
// Simple world state: "x,y,z" -> block id
|
|
240
|
+
world: Map<string, string> = new Map()
|
|
241
|
+
|
|
242
|
+
// Current weather
|
|
243
|
+
weather: string = 'clear'
|
|
244
|
+
|
|
245
|
+
// Current world time
|
|
246
|
+
worldTime: number = 0
|
|
247
|
+
|
|
248
|
+
// Active potion effects by entity id
|
|
249
|
+
effects: Map<string, { effect: string; duration: number; amplifier: number }[]> = new Map()
|
|
250
|
+
|
|
251
|
+
// XP values by player/entity id
|
|
252
|
+
xp: Map<string, number> = new Map()
|
|
253
|
+
|
|
254
|
+
// Tick counter
|
|
255
|
+
tickCount: number = 0
|
|
256
|
+
|
|
257
|
+
// Namespace
|
|
258
|
+
namespace: string
|
|
259
|
+
|
|
260
|
+
// Entity ID counter
|
|
261
|
+
private entityIdCounter = 0
|
|
262
|
+
|
|
263
|
+
// Return value for current function
|
|
264
|
+
private returnValue: number | undefined
|
|
265
|
+
|
|
266
|
+
// Flag to stop function execution (for return)
|
|
267
|
+
private shouldReturn: boolean = false
|
|
268
|
+
|
|
269
|
+
constructor(namespace: string) {
|
|
270
|
+
this.namespace = namespace
|
|
271
|
+
// Initialize default objective
|
|
272
|
+
this.scoreboard.set('rs', new Map())
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// -------------------------------------------------------------------------
|
|
276
|
+
// Datapack Loading
|
|
277
|
+
// -------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
loadDatapack(dir: string): void {
|
|
280
|
+
const functionsDir = path.join(dir, 'data', this.namespace, 'function')
|
|
281
|
+
if (!fs.existsSync(functionsDir)) return
|
|
282
|
+
|
|
283
|
+
const loadFunctions = (base: string, prefix: string): void => {
|
|
284
|
+
const entries = fs.readdirSync(base, { withFileTypes: true })
|
|
285
|
+
for (const entry of entries) {
|
|
286
|
+
const fullPath = path.join(base, entry.name)
|
|
287
|
+
if (entry.isDirectory()) {
|
|
288
|
+
loadFunctions(fullPath, `${prefix}${entry.name}/`)
|
|
289
|
+
} else if (entry.name.endsWith('.mcfunction')) {
|
|
290
|
+
const fnName = `${prefix}${entry.name.replace('.mcfunction', '')}`
|
|
291
|
+
const content = fs.readFileSync(fullPath, 'utf-8')
|
|
292
|
+
this.loadFunction(`${this.namespace}:${fnName}`, content.split('\n'))
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
loadFunctions(functionsDir, '')
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
loadFunction(name: string, lines: string[]): void {
|
|
301
|
+
// Filter out comments and empty lines, but keep all commands
|
|
302
|
+
const cleaned = lines
|
|
303
|
+
.map(l => l.trim())
|
|
304
|
+
.filter(l => l && !l.startsWith('#'))
|
|
305
|
+
this.functions.set(name, cleaned)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// -------------------------------------------------------------------------
|
|
309
|
+
// Lifecycle Methods
|
|
310
|
+
// -------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
load(): void {
|
|
313
|
+
const loadFn = `${this.namespace}:__load`
|
|
314
|
+
if (this.functions.has(loadFn)) {
|
|
315
|
+
this.execFunction(loadFn)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
tick(): void {
|
|
320
|
+
this.tickCount++
|
|
321
|
+
const tickFn = `${this.namespace}:__tick`
|
|
322
|
+
if (this.functions.has(tickFn)) {
|
|
323
|
+
this.execFunction(tickFn)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
ticks(n: number): void {
|
|
328
|
+
for (let i = 0; i < n; i++) {
|
|
329
|
+
this.tick()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// -------------------------------------------------------------------------
|
|
334
|
+
// Function Execution
|
|
335
|
+
// -------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
execFunction(name: string, executor?: Entity): void {
|
|
338
|
+
const lines = this.functions.get(name)
|
|
339
|
+
if (!lines) {
|
|
340
|
+
// Try with namespace prefix
|
|
341
|
+
const prefixedName = name.includes(':') ? name : `${this.namespace}:${name}`
|
|
342
|
+
const prefixedLines = this.functions.get(prefixedName)
|
|
343
|
+
if (!prefixedLines) return
|
|
344
|
+
this.execFunctionLines(prefixedLines, executor)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
this.execFunctionLines(lines, executor)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private execFunctionLines(lines: string[], executor?: Entity): void {
|
|
351
|
+
this.shouldReturn = false
|
|
352
|
+
for (const line of lines) {
|
|
353
|
+
if (this.shouldReturn) break
|
|
354
|
+
this.execCommand(line, executor)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// -------------------------------------------------------------------------
|
|
359
|
+
// Command Execution
|
|
360
|
+
// -------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
execCommand(cmd: string, executor?: Entity): boolean {
|
|
363
|
+
cmd = cmd.trim()
|
|
364
|
+
if (!cmd || cmd.startsWith('#')) return true
|
|
365
|
+
|
|
366
|
+
// Parse command
|
|
367
|
+
if (cmd.startsWith('scoreboard ')) {
|
|
368
|
+
return this.execScoreboard(cmd)
|
|
369
|
+
}
|
|
370
|
+
if (cmd.startsWith('execute ')) {
|
|
371
|
+
return this.execExecute(cmd, executor)
|
|
372
|
+
}
|
|
373
|
+
if (cmd.startsWith('function ')) {
|
|
374
|
+
return this.execFunctionCmd(cmd, executor)
|
|
375
|
+
}
|
|
376
|
+
if (cmd.startsWith('data ')) {
|
|
377
|
+
return this.execData(cmd)
|
|
378
|
+
}
|
|
379
|
+
if (cmd.startsWith('tag ')) {
|
|
380
|
+
return this.execTag(cmd, executor)
|
|
381
|
+
}
|
|
382
|
+
if (cmd.startsWith('say ')) {
|
|
383
|
+
return this.execSay(cmd, executor)
|
|
384
|
+
}
|
|
385
|
+
if (cmd.startsWith('tellraw ')) {
|
|
386
|
+
return this.execTellraw(cmd)
|
|
387
|
+
}
|
|
388
|
+
if (cmd.startsWith('title ')) {
|
|
389
|
+
return this.execTitle(cmd)
|
|
390
|
+
}
|
|
391
|
+
if (cmd.startsWith('setblock ')) {
|
|
392
|
+
return this.execSetblock(cmd)
|
|
393
|
+
}
|
|
394
|
+
if (cmd.startsWith('fill ')) {
|
|
395
|
+
return this.execFill(cmd)
|
|
396
|
+
}
|
|
397
|
+
if (cmd.startsWith('tp ')) {
|
|
398
|
+
return this.execTp(cmd, executor)
|
|
399
|
+
}
|
|
400
|
+
if (cmd.startsWith('weather ')) {
|
|
401
|
+
return this.execWeather(cmd)
|
|
402
|
+
}
|
|
403
|
+
if (cmd.startsWith('time ')) {
|
|
404
|
+
return this.execTime(cmd)
|
|
405
|
+
}
|
|
406
|
+
if (cmd.startsWith('kill ')) {
|
|
407
|
+
return this.execKill(cmd, executor)
|
|
408
|
+
}
|
|
409
|
+
if (cmd.startsWith('effect ')) {
|
|
410
|
+
return this.execEffect(cmd, executor)
|
|
411
|
+
}
|
|
412
|
+
if (cmd.startsWith('xp ')) {
|
|
413
|
+
return this.execXp(cmd, executor)
|
|
414
|
+
}
|
|
415
|
+
if (cmd.startsWith('summon ')) {
|
|
416
|
+
return this.execSummon(cmd)
|
|
417
|
+
}
|
|
418
|
+
if (cmd.startsWith('return ')) {
|
|
419
|
+
return this.execReturn(cmd, executor)
|
|
420
|
+
}
|
|
421
|
+
if (cmd === 'return') {
|
|
422
|
+
this.shouldReturn = true
|
|
423
|
+
return true
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Unknown command - succeed silently
|
|
427
|
+
return true
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// -------------------------------------------------------------------------
|
|
431
|
+
// Scoreboard Commands
|
|
432
|
+
// -------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
private execScoreboard(cmd: string): boolean {
|
|
435
|
+
const parts = cmd.split(/\s+/)
|
|
436
|
+
|
|
437
|
+
// scoreboard objectives add <name> <criteria>
|
|
438
|
+
if (parts[1] === 'objectives' && parts[2] === 'add') {
|
|
439
|
+
const name = parts[3]
|
|
440
|
+
if (!this.scoreboard.has(name)) {
|
|
441
|
+
this.scoreboard.set(name, new Map())
|
|
442
|
+
}
|
|
443
|
+
return true
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// scoreboard players ...
|
|
447
|
+
if (parts[1] === 'players') {
|
|
448
|
+
const action = parts[2]
|
|
449
|
+
const player = parts[3]
|
|
450
|
+
const objective = parts[4]
|
|
451
|
+
|
|
452
|
+
switch (action) {
|
|
453
|
+
case 'set': {
|
|
454
|
+
const value = parseInt(parts[5], 10)
|
|
455
|
+
this.setScore(player, objective, value)
|
|
456
|
+
return true
|
|
457
|
+
}
|
|
458
|
+
case 'add': {
|
|
459
|
+
const delta = parseInt(parts[5], 10)
|
|
460
|
+
this.addScore(player, objective, delta)
|
|
461
|
+
return true
|
|
462
|
+
}
|
|
463
|
+
case 'remove': {
|
|
464
|
+
const delta = parseInt(parts[5], 10)
|
|
465
|
+
this.addScore(player, objective, -delta)
|
|
466
|
+
return true
|
|
467
|
+
}
|
|
468
|
+
case 'get': {
|
|
469
|
+
this.returnValue = this.getScore(player, objective)
|
|
470
|
+
return true
|
|
471
|
+
}
|
|
472
|
+
case 'reset': {
|
|
473
|
+
const obj = this.scoreboard.get(objective)
|
|
474
|
+
if (obj) obj.delete(player)
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
477
|
+
case 'enable': {
|
|
478
|
+
// No-op for trigger enabling
|
|
479
|
+
return true
|
|
480
|
+
}
|
|
481
|
+
case 'operation': {
|
|
482
|
+
// scoreboard players operation <target> <targetObj> <op> <source> <sourceObj>
|
|
483
|
+
const targetObj = objective
|
|
484
|
+
const op = parts[5]
|
|
485
|
+
const source = parts[6]
|
|
486
|
+
const sourceObj = parts[7]
|
|
487
|
+
|
|
488
|
+
const targetVal = this.getScore(player, targetObj)
|
|
489
|
+
const sourceVal = this.getScore(source, sourceObj)
|
|
490
|
+
|
|
491
|
+
let result: number
|
|
492
|
+
switch (op) {
|
|
493
|
+
case '=':
|
|
494
|
+
result = sourceVal
|
|
495
|
+
break
|
|
496
|
+
case '+=':
|
|
497
|
+
result = targetVal + sourceVal
|
|
498
|
+
break
|
|
499
|
+
case '-=':
|
|
500
|
+
result = targetVal - sourceVal
|
|
501
|
+
break
|
|
502
|
+
case '*=':
|
|
503
|
+
result = targetVal * sourceVal
|
|
504
|
+
break
|
|
505
|
+
case '/=':
|
|
506
|
+
result = Math.trunc(targetVal / sourceVal)
|
|
507
|
+
break
|
|
508
|
+
case '%=':
|
|
509
|
+
result = targetVal % sourceVal // Java modulo: sign follows dividend
|
|
510
|
+
break
|
|
511
|
+
case '<':
|
|
512
|
+
result = Math.min(targetVal, sourceVal)
|
|
513
|
+
break
|
|
514
|
+
case '>':
|
|
515
|
+
result = Math.max(targetVal, sourceVal)
|
|
516
|
+
break
|
|
517
|
+
case '><':
|
|
518
|
+
// Swap
|
|
519
|
+
this.setScore(player, targetObj, sourceVal)
|
|
520
|
+
this.setScore(source, sourceObj, targetVal)
|
|
521
|
+
return true
|
|
522
|
+
default:
|
|
523
|
+
return false
|
|
524
|
+
}
|
|
525
|
+
this.setScore(player, targetObj, result)
|
|
526
|
+
return true
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return false
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// -------------------------------------------------------------------------
|
|
535
|
+
// Execute Commands
|
|
536
|
+
// -------------------------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
private execExecute(cmd: string, executor?: Entity): boolean {
|
|
539
|
+
// Remove 'execute ' prefix
|
|
540
|
+
let rest = cmd.slice(8)
|
|
541
|
+
|
|
542
|
+
// Track execute state
|
|
543
|
+
let currentExecutor = executor
|
|
544
|
+
let condition: boolean = true
|
|
545
|
+
let storeTarget: { player: string; objective: string; type: 'result' | 'success' } | null = null
|
|
546
|
+
|
|
547
|
+
while (rest.length > 0) {
|
|
548
|
+
rest = rest.trimStart()
|
|
549
|
+
|
|
550
|
+
// Handle 'run' - execute the final command
|
|
551
|
+
if (rest.startsWith('run ')) {
|
|
552
|
+
if (!condition) return false
|
|
553
|
+
const innerCmd = rest.slice(4)
|
|
554
|
+
const result = this.execCommand(innerCmd, currentExecutor)
|
|
555
|
+
|
|
556
|
+
if (storeTarget) {
|
|
557
|
+
const value = storeTarget.type === 'result'
|
|
558
|
+
? (this.returnValue ?? (result ? 1 : 0))
|
|
559
|
+
: (result ? 1 : 0)
|
|
560
|
+
this.setScore(storeTarget.player, storeTarget.objective, value)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return result
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Handle 'as <selector>'
|
|
567
|
+
if (rest.startsWith('as ')) {
|
|
568
|
+
rest = rest.slice(3)
|
|
569
|
+
const { selector, remaining } = this.parseNextSelector(rest)
|
|
570
|
+
rest = remaining
|
|
571
|
+
|
|
572
|
+
const entities = parseSelector(selector, this.entities, currentExecutor)
|
|
573
|
+
if (entities.length === 0) return false
|
|
574
|
+
|
|
575
|
+
// For multiple entities, execute as each
|
|
576
|
+
if (entities.length > 1) {
|
|
577
|
+
let success = false
|
|
578
|
+
for (const entity of entities) {
|
|
579
|
+
const result = this.execCommand('execute ' + rest, entity)
|
|
580
|
+
success = success || result
|
|
581
|
+
}
|
|
582
|
+
return success
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
currentExecutor = entities[0]
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Handle 'at <selector>' - no-op for position, just continue
|
|
590
|
+
if (rest.startsWith('at ')) {
|
|
591
|
+
rest = rest.slice(3)
|
|
592
|
+
const { remaining } = this.parseNextSelector(rest)
|
|
593
|
+
rest = remaining
|
|
594
|
+
continue
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Handle 'if score <player> <obj> matches <range>'
|
|
598
|
+
if (rest.startsWith('if score ')) {
|
|
599
|
+
rest = rest.slice(9)
|
|
600
|
+
const scoreParts = rest.match(/^(\S+)\s+(\S+)\s+matches\s+(\S+)(.*)$/)
|
|
601
|
+
if (scoreParts) {
|
|
602
|
+
const [, player, obj, rangeStr, remaining] = scoreParts
|
|
603
|
+
const range = parseRange(rangeStr)
|
|
604
|
+
const score = this.getScore(player, obj)
|
|
605
|
+
condition = condition && matchesRange(score, range)
|
|
606
|
+
rest = remaining.trim()
|
|
607
|
+
continue
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// if score <p1> <o1> <op> <p2> <o2>
|
|
611
|
+
const compareMatch = rest.match(/^(\S+)\s+(\S+)\s+([<>=]+)\s+(\S+)\s+(\S+)(.*)$/)
|
|
612
|
+
if (compareMatch) {
|
|
613
|
+
const [, p1, o1, op, p2, o2, remaining] = compareMatch
|
|
614
|
+
const v1 = this.getScore(p1, o1)
|
|
615
|
+
const v2 = this.getScore(p2, o2)
|
|
616
|
+
let matches = false
|
|
617
|
+
switch (op) {
|
|
618
|
+
case '=': matches = v1 === v2; break
|
|
619
|
+
case '<': matches = v1 < v2; break
|
|
620
|
+
case '<=': matches = v1 <= v2; break
|
|
621
|
+
case '>': matches = v1 > v2; break
|
|
622
|
+
case '>=': matches = v1 >= v2; break
|
|
623
|
+
}
|
|
624
|
+
condition = condition && matches
|
|
625
|
+
rest = remaining.trim()
|
|
626
|
+
continue
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Handle 'unless score ...'
|
|
631
|
+
if (rest.startsWith('unless score ')) {
|
|
632
|
+
rest = rest.slice(13)
|
|
633
|
+
const scoreParts = rest.match(/^(\S+)\s+(\S+)\s+matches\s+(\S+)(.*)$/)
|
|
634
|
+
if (scoreParts) {
|
|
635
|
+
const [, player, obj, rangeStr, remaining] = scoreParts
|
|
636
|
+
const range = parseRange(rangeStr)
|
|
637
|
+
const score = this.getScore(player, obj)
|
|
638
|
+
condition = condition && !matchesRange(score, range)
|
|
639
|
+
rest = remaining.trim()
|
|
640
|
+
continue
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Handle 'if entity <selector>'
|
|
645
|
+
if (rest.startsWith('if entity ')) {
|
|
646
|
+
rest = rest.slice(10)
|
|
647
|
+
const { selector, remaining } = this.parseNextSelector(rest)
|
|
648
|
+
rest = remaining
|
|
649
|
+
const entities = parseSelector(selector, this.entities, currentExecutor)
|
|
650
|
+
condition = condition && entities.length > 0
|
|
651
|
+
continue
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Handle 'unless entity <selector>'
|
|
655
|
+
if (rest.startsWith('unless entity ')) {
|
|
656
|
+
rest = rest.slice(14)
|
|
657
|
+
const { selector, remaining } = this.parseNextSelector(rest)
|
|
658
|
+
rest = remaining
|
|
659
|
+
const entities = parseSelector(selector, this.entities, currentExecutor)
|
|
660
|
+
condition = condition && entities.length === 0
|
|
661
|
+
continue
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Handle 'store result score <player> <obj>'
|
|
665
|
+
if (rest.startsWith('store result score ')) {
|
|
666
|
+
rest = rest.slice(19)
|
|
667
|
+
const storeParts = rest.match(/^(\S+)\s+(\S+)(.*)$/)
|
|
668
|
+
if (storeParts) {
|
|
669
|
+
const [, player, obj, remaining] = storeParts
|
|
670
|
+
storeTarget = { player, objective: obj, type: 'result' }
|
|
671
|
+
rest = remaining.trim()
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Handle 'store success score <player> <obj>'
|
|
677
|
+
if (rest.startsWith('store success score ')) {
|
|
678
|
+
rest = rest.slice(20)
|
|
679
|
+
const storeParts = rest.match(/^(\S+)\s+(\S+)(.*)$/)
|
|
680
|
+
if (storeParts) {
|
|
681
|
+
const [, player, obj, remaining] = storeParts
|
|
682
|
+
storeTarget = { player, objective: obj, type: 'success' }
|
|
683
|
+
rest = remaining.trim()
|
|
684
|
+
continue
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Unknown subcommand - skip to next space or 'run'
|
|
689
|
+
const nextSpace = rest.indexOf(' ')
|
|
690
|
+
if (nextSpace === -1) break
|
|
691
|
+
rest = rest.slice(nextSpace + 1)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (storeTarget) {
|
|
695
|
+
const value = storeTarget.type === 'result'
|
|
696
|
+
? (this.returnValue ?? (condition ? 1 : 0))
|
|
697
|
+
: (condition ? 1 : 0)
|
|
698
|
+
this.setScore(storeTarget.player, storeTarget.objective, value)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return condition
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private parseNextSelector(input: string): { selector: string; remaining: string } {
|
|
705
|
+
input = input.trimStart()
|
|
706
|
+
const match = input.match(/^(@[eaps])(\[[^\]]*\])?/)
|
|
707
|
+
if (match) {
|
|
708
|
+
const selector = match[0]
|
|
709
|
+
return { selector, remaining: input.slice(selector.length).trim() }
|
|
710
|
+
}
|
|
711
|
+
// Non-selector target
|
|
712
|
+
const spaceIdx = input.indexOf(' ')
|
|
713
|
+
if (spaceIdx === -1) {
|
|
714
|
+
return { selector: input, remaining: '' }
|
|
715
|
+
}
|
|
716
|
+
return { selector: input.slice(0, spaceIdx), remaining: input.slice(spaceIdx + 1) }
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// -------------------------------------------------------------------------
|
|
720
|
+
// Function Command
|
|
721
|
+
// -------------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
private execFunctionCmd(cmd: string, executor?: Entity): boolean {
|
|
724
|
+
const fnName = cmd.slice(9).trim() // remove 'function '
|
|
725
|
+
const outerShouldReturn = this.shouldReturn
|
|
726
|
+
this.execFunction(fnName, executor)
|
|
727
|
+
this.shouldReturn = outerShouldReturn
|
|
728
|
+
return true
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// -------------------------------------------------------------------------
|
|
732
|
+
// Data Commands
|
|
733
|
+
// -------------------------------------------------------------------------
|
|
734
|
+
|
|
735
|
+
private execData(cmd: string): boolean {
|
|
736
|
+
// data modify storage <ns:path> <field> set value <val>
|
|
737
|
+
const setMatch = cmd.match(/^data modify storage (\S+) (\S+) set value (.+)$/)
|
|
738
|
+
if (setMatch) {
|
|
739
|
+
const [, storagePath, field, valueStr] = setMatch
|
|
740
|
+
const value = this.parseDataValue(valueStr)
|
|
741
|
+
this.setStorageField(storagePath, field, value)
|
|
742
|
+
return true
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// data modify storage <ns:path> <field> append value <val>
|
|
746
|
+
const appendMatch = cmd.match(/^data modify storage (\S+) (\S+) append value (.+)$/)
|
|
747
|
+
if (appendMatch) {
|
|
748
|
+
const [, storagePath, field, valueStr] = appendMatch
|
|
749
|
+
const value = this.parseDataValue(valueStr)
|
|
750
|
+
const current = this.getStorageField(storagePath, field) ?? []
|
|
751
|
+
if (Array.isArray(current)) {
|
|
752
|
+
current.push(value)
|
|
753
|
+
this.setStorageField(storagePath, field, current)
|
|
754
|
+
}
|
|
755
|
+
return true
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// data get storage <ns:path> <field>
|
|
759
|
+
const getMatch = cmd.match(/^data get storage (\S+) (\S+)$/)
|
|
760
|
+
if (getMatch) {
|
|
761
|
+
const [, storagePath, field] = getMatch
|
|
762
|
+
const value = this.getStorageField(storagePath, field)
|
|
763
|
+
if (typeof value === 'number') {
|
|
764
|
+
this.returnValue = value
|
|
765
|
+
} else if (Array.isArray(value)) {
|
|
766
|
+
this.returnValue = value.length
|
|
767
|
+
} else {
|
|
768
|
+
this.returnValue = value ? 1 : 0
|
|
769
|
+
}
|
|
770
|
+
return true
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// data modify storage <ns:path> <field> set from storage <src> <srcpath>
|
|
774
|
+
const copyMatch = cmd.match(/^data modify storage (\S+) (\S+) set from storage (\S+) (\S+)$/)
|
|
775
|
+
if (copyMatch) {
|
|
776
|
+
const [, dstPath, dstField, srcPath, srcField] = copyMatch
|
|
777
|
+
const value = this.getStorageField(srcPath, srcField)
|
|
778
|
+
this.setStorageField(dstPath, dstField, value)
|
|
779
|
+
return true
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// data remove storage <ns:path> <field>
|
|
783
|
+
const removeMatch = cmd.match(/^data remove storage (\S+) (\S+)$/)
|
|
784
|
+
if (removeMatch) {
|
|
785
|
+
const [, storagePath, field] = removeMatch
|
|
786
|
+
return this.removeStorageField(storagePath, field)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return false
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private parseDataValue(str: string): any {
|
|
793
|
+
str = str.trim()
|
|
794
|
+
// Try JSON parse
|
|
795
|
+
try {
|
|
796
|
+
return JSON.parse(str)
|
|
797
|
+
} catch {
|
|
798
|
+
// Try numeric
|
|
799
|
+
const num = parseFloat(str)
|
|
800
|
+
if (!isNaN(num)) return num
|
|
801
|
+
// Return as string
|
|
802
|
+
return str
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private getStorageField(storagePath: string, field: string): any {
|
|
807
|
+
const data = this.storage.get(storagePath) ?? {}
|
|
808
|
+
const segments = this.parseStoragePath(field)
|
|
809
|
+
let current: any = data
|
|
810
|
+
for (const segment of segments) {
|
|
811
|
+
if (typeof segment === 'number') {
|
|
812
|
+
if (!Array.isArray(current)) return undefined
|
|
813
|
+
const index = segment < 0 ? current.length + segment : segment
|
|
814
|
+
current = current[index]
|
|
815
|
+
continue
|
|
816
|
+
}
|
|
817
|
+
if (current == null || typeof current !== 'object') return undefined
|
|
818
|
+
current = current[segment]
|
|
819
|
+
}
|
|
820
|
+
return current
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private setStorageField(storagePath: string, field: string, value: any): void {
|
|
824
|
+
let data = this.storage.get(storagePath)
|
|
825
|
+
if (!data) {
|
|
826
|
+
data = {}
|
|
827
|
+
this.storage.set(storagePath, data)
|
|
828
|
+
}
|
|
829
|
+
const segments = this.parseStoragePath(field)
|
|
830
|
+
let current: any = data
|
|
831
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
832
|
+
const segment = segments[i]
|
|
833
|
+
const next = segments[i + 1]
|
|
834
|
+
if (typeof segment === 'number') {
|
|
835
|
+
if (!Array.isArray(current)) return
|
|
836
|
+
const index = segment < 0 ? current.length + segment : segment
|
|
837
|
+
if (current[index] === undefined) {
|
|
838
|
+
current[index] = typeof next === 'number' ? [] : {}
|
|
839
|
+
}
|
|
840
|
+
current = current[index]
|
|
841
|
+
continue
|
|
842
|
+
}
|
|
843
|
+
if (!(segment in current)) {
|
|
844
|
+
current[segment] = typeof next === 'number' ? [] : {}
|
|
845
|
+
}
|
|
846
|
+
current = current[segment]
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const last = segments[segments.length - 1]
|
|
850
|
+
if (typeof last === 'number') {
|
|
851
|
+
if (!Array.isArray(current)) return
|
|
852
|
+
const index = last < 0 ? current.length + last : last
|
|
853
|
+
current[index] = value
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
current[last] = value
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private removeStorageField(storagePath: string, field: string): boolean {
|
|
860
|
+
const data = this.storage.get(storagePath)
|
|
861
|
+
if (!data) return false
|
|
862
|
+
|
|
863
|
+
const segments = this.parseStoragePath(field)
|
|
864
|
+
let current: any = data
|
|
865
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
866
|
+
const segment = segments[i]
|
|
867
|
+
if (typeof segment === 'number') {
|
|
868
|
+
if (!Array.isArray(current)) return false
|
|
869
|
+
const index = segment < 0 ? current.length + segment : segment
|
|
870
|
+
current = current[index]
|
|
871
|
+
} else {
|
|
872
|
+
current = current?.[segment]
|
|
873
|
+
}
|
|
874
|
+
if (current === undefined) return false
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const last = segments[segments.length - 1]
|
|
878
|
+
if (typeof last === 'number') {
|
|
879
|
+
if (!Array.isArray(current)) return false
|
|
880
|
+
const index = last < 0 ? current.length + last : last
|
|
881
|
+
if (index < 0 || index >= current.length) return false
|
|
882
|
+
current.splice(index, 1)
|
|
883
|
+
return true
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (current == null || typeof current !== 'object' || !(last in current)) return false
|
|
887
|
+
delete current[last]
|
|
888
|
+
return true
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private parseStoragePath(field: string): Array<string | number> {
|
|
892
|
+
return field
|
|
893
|
+
.split('.')
|
|
894
|
+
.flatMap(part => {
|
|
895
|
+
const segments: Array<string | number> = []
|
|
896
|
+
const regex = /([^\[\]]+)|\[(-?\d+)\]/g
|
|
897
|
+
for (const match of part.matchAll(regex)) {
|
|
898
|
+
if (match[1]) segments.push(match[1])
|
|
899
|
+
if (match[2]) segments.push(parseInt(match[2], 10))
|
|
900
|
+
}
|
|
901
|
+
return segments
|
|
902
|
+
})
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// -------------------------------------------------------------------------
|
|
906
|
+
// Tag Commands
|
|
907
|
+
// -------------------------------------------------------------------------
|
|
908
|
+
|
|
909
|
+
private execTag(cmd: string, executor?: Entity): boolean {
|
|
910
|
+
// tag <selector> add <name>
|
|
911
|
+
const addMatch = cmd.match(/^tag (\S+) add (\S+)$/)
|
|
912
|
+
if (addMatch) {
|
|
913
|
+
const [, selStr, tagName] = addMatch
|
|
914
|
+
const entities = selStr === '@s' && executor
|
|
915
|
+
? [executor]
|
|
916
|
+
: parseSelector(selStr, this.entities, executor)
|
|
917
|
+
for (const entity of entities) {
|
|
918
|
+
entity.tags.add(tagName)
|
|
919
|
+
}
|
|
920
|
+
return entities.length > 0
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// tag <selector> remove <name>
|
|
924
|
+
const removeMatch = cmd.match(/^tag (\S+) remove (\S+)$/)
|
|
925
|
+
if (removeMatch) {
|
|
926
|
+
const [, selStr, tagName] = removeMatch
|
|
927
|
+
const entities = selStr === '@s' && executor
|
|
928
|
+
? [executor]
|
|
929
|
+
: parseSelector(selStr, this.entities, executor)
|
|
930
|
+
for (const entity of entities) {
|
|
931
|
+
entity.tags.delete(tagName)
|
|
932
|
+
}
|
|
933
|
+
return entities.length > 0
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return false
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// -------------------------------------------------------------------------
|
|
940
|
+
// Say/Tellraw/Title Commands
|
|
941
|
+
// -------------------------------------------------------------------------
|
|
942
|
+
|
|
943
|
+
private execSay(cmd: string, executor?: Entity): boolean {
|
|
944
|
+
const message = cmd.slice(4)
|
|
945
|
+
this.chatLog.push(`[${executor?.id ?? 'Server'}] ${message}`)
|
|
946
|
+
return true
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private execTellraw(cmd: string): boolean {
|
|
950
|
+
// tellraw <selector> <json>
|
|
951
|
+
const match = cmd.match(/^tellraw \S+ (.+)$/)
|
|
952
|
+
if (match) {
|
|
953
|
+
const jsonStr = match[1]
|
|
954
|
+
const text = this.extractJsonText(jsonStr)
|
|
955
|
+
this.chatLog.push(text)
|
|
956
|
+
return true
|
|
957
|
+
}
|
|
958
|
+
return false
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private execTitle(cmd: string): boolean {
|
|
962
|
+
// title <selector> <kind> <json>
|
|
963
|
+
const match = cmd.match(/^title \S+ (actionbar|title|subtitle) (.+)$/)
|
|
964
|
+
if (match) {
|
|
965
|
+
const [, kind, jsonStr] = match
|
|
966
|
+
const text = this.extractJsonText(jsonStr)
|
|
967
|
+
this.chatLog.push(`[${kind.toUpperCase()}] ${text}`)
|
|
968
|
+
return true
|
|
969
|
+
}
|
|
970
|
+
return false
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private extractJsonText(json: any): string {
|
|
974
|
+
if (typeof json === 'string') {
|
|
975
|
+
try {
|
|
976
|
+
json = JSON.parse(json)
|
|
977
|
+
} catch {
|
|
978
|
+
return json
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (typeof json === 'string') return json
|
|
983
|
+
if (Array.isArray(json)) {
|
|
984
|
+
return json.map(part => this.extractJsonText(part)).join('')
|
|
985
|
+
}
|
|
986
|
+
if (typeof json === 'object' && json !== null) {
|
|
987
|
+
if ('text' in json) return String(json.text)
|
|
988
|
+
if ('score' in json && typeof json.score === 'object' && json.score !== null) {
|
|
989
|
+
const name = 'name' in json.score ? String(json.score.name) : ''
|
|
990
|
+
const objective = 'objective' in json.score ? String(json.score.objective) : 'rs'
|
|
991
|
+
return String(this.getScore(name, objective))
|
|
992
|
+
}
|
|
993
|
+
if ('extra' in json && Array.isArray(json.extra)) {
|
|
994
|
+
return json.extra.map((part: any) => this.extractJsonText(part)).join('')
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return ''
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// -------------------------------------------------------------------------
|
|
1001
|
+
// World Commands
|
|
1002
|
+
// -------------------------------------------------------------------------
|
|
1003
|
+
|
|
1004
|
+
private execSetblock(cmd: string): boolean {
|
|
1005
|
+
const match = cmd.match(/^setblock (\S+) (\S+) (\S+) (\S+)$/)
|
|
1006
|
+
if (!match) return false
|
|
1007
|
+
|
|
1008
|
+
const [, x, y, z, block] = match
|
|
1009
|
+
const key = this.positionKey(x, y, z)
|
|
1010
|
+
if (!key) return false
|
|
1011
|
+
this.world.set(key, block)
|
|
1012
|
+
return true
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private execFill(cmd: string): boolean {
|
|
1016
|
+
const match = cmd.match(/^fill (\S+) (\S+) (\S+) (\S+) (\S+) (\S+) (\S+)$/)
|
|
1017
|
+
if (!match) return false
|
|
1018
|
+
|
|
1019
|
+
const [, x1, y1, z1, x2, y2, z2, block] = match
|
|
1020
|
+
const start = this.parseAbsolutePosition(x1, y1, z1)
|
|
1021
|
+
const end = this.parseAbsolutePosition(x2, y2, z2)
|
|
1022
|
+
if (!start || !end) return false
|
|
1023
|
+
|
|
1024
|
+
const [minX, maxX] = [Math.min(start.x, end.x), Math.max(start.x, end.x)]
|
|
1025
|
+
const [minY, maxY] = [Math.min(start.y, end.y), Math.max(start.y, end.y)]
|
|
1026
|
+
const [minZ, maxZ] = [Math.min(start.z, end.z), Math.max(start.z, end.z)]
|
|
1027
|
+
|
|
1028
|
+
for (let x = minX; x <= maxX; x++) {
|
|
1029
|
+
for (let y = minY; y <= maxY; y++) {
|
|
1030
|
+
for (let z = minZ; z <= maxZ; z++) {
|
|
1031
|
+
this.world.set(`${x},${y},${z}`, block)
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return true
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private execTp(cmd: string, executor?: Entity): boolean {
|
|
1039
|
+
const selfCoordsMatch = cmd.match(/^tp (\S+) (\S+) (\S+)$/)
|
|
1040
|
+
if (selfCoordsMatch && executor) {
|
|
1041
|
+
const [, x, y, z] = selfCoordsMatch
|
|
1042
|
+
const next = this.resolvePosition(executor.position ?? { x: 0, y: 0, z: 0 }, x, y, z)
|
|
1043
|
+
if (!next) return false
|
|
1044
|
+
executor.position = next
|
|
1045
|
+
return true
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const coordsMatch = cmd.match(/^tp (\S+) (\S+) (\S+) (\S+)$/)
|
|
1049
|
+
if (coordsMatch) {
|
|
1050
|
+
const [, selStr, x, y, z] = coordsMatch
|
|
1051
|
+
const entities = selStr === '@s' && executor
|
|
1052
|
+
? [executor]
|
|
1053
|
+
: parseSelector(selStr, this.entities, executor)
|
|
1054
|
+
for (const entity of entities) {
|
|
1055
|
+
const next = this.resolvePosition(entity.position ?? { x: 0, y: 0, z: 0 }, x, y, z)
|
|
1056
|
+
if (next) {
|
|
1057
|
+
entity.position = next
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return entities.length > 0
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const entityMatch = cmd.match(/^tp (\S+) (\S+)$/)
|
|
1064
|
+
if (entityMatch) {
|
|
1065
|
+
const [, selStr, targetStr] = entityMatch
|
|
1066
|
+
const entities = selStr === '@s' && executor
|
|
1067
|
+
? [executor]
|
|
1068
|
+
: parseSelector(selStr, this.entities, executor)
|
|
1069
|
+
const target = targetStr === '@s' && executor
|
|
1070
|
+
? executor
|
|
1071
|
+
: parseSelector(targetStr, this.entities, executor)[0]
|
|
1072
|
+
if (!target?.position) return false
|
|
1073
|
+
for (const entity of entities) {
|
|
1074
|
+
entity.position = { ...target.position }
|
|
1075
|
+
}
|
|
1076
|
+
return entities.length > 0
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return false
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private execWeather(cmd: string): boolean {
|
|
1083
|
+
const match = cmd.match(/^weather (\S+)$/)
|
|
1084
|
+
if (!match) return false
|
|
1085
|
+
this.weather = match[1]
|
|
1086
|
+
return true
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private execTime(cmd: string): boolean {
|
|
1090
|
+
const match = cmd.match(/^time (set|add) (\S+)$/)
|
|
1091
|
+
if (!match) return false
|
|
1092
|
+
|
|
1093
|
+
const [, action, valueStr] = match
|
|
1094
|
+
const value = this.parseTimeValue(valueStr)
|
|
1095
|
+
if (value === null) return false
|
|
1096
|
+
|
|
1097
|
+
if (action === 'set') {
|
|
1098
|
+
this.worldTime = value
|
|
1099
|
+
} else {
|
|
1100
|
+
this.worldTime += value
|
|
1101
|
+
}
|
|
1102
|
+
return true
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// -------------------------------------------------------------------------
|
|
1106
|
+
// Kill Command
|
|
1107
|
+
// -------------------------------------------------------------------------
|
|
1108
|
+
|
|
1109
|
+
private execKill(cmd: string, executor?: Entity): boolean {
|
|
1110
|
+
const selStr = cmd.slice(5).trim()
|
|
1111
|
+
|
|
1112
|
+
if (selStr === '@s' && executor) {
|
|
1113
|
+
this.entities = this.entities.filter(e => e !== executor)
|
|
1114
|
+
return true
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const entities = parseSelector(selStr, this.entities, executor)
|
|
1118
|
+
for (const entity of entities) {
|
|
1119
|
+
this.entities = this.entities.filter(e => e !== entity)
|
|
1120
|
+
}
|
|
1121
|
+
return entities.length > 0
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// -------------------------------------------------------------------------
|
|
1125
|
+
// Effect / XP Commands
|
|
1126
|
+
// -------------------------------------------------------------------------
|
|
1127
|
+
|
|
1128
|
+
private execEffect(cmd: string, executor?: Entity): boolean {
|
|
1129
|
+
const match = cmd.match(/^effect give (\S+) (\S+)(?: (\S+))?(?: (\S+))?(?: \S+)?$/)
|
|
1130
|
+
if (!match) return false
|
|
1131
|
+
|
|
1132
|
+
const [, selStr, effect, durationStr, amplifierStr] = match
|
|
1133
|
+
const entities = selStr === '@s' && executor
|
|
1134
|
+
? [executor]
|
|
1135
|
+
: parseSelector(selStr, this.entities, executor)
|
|
1136
|
+
|
|
1137
|
+
const duration = durationStr ? parseInt(durationStr, 10) : 30
|
|
1138
|
+
const amplifier = amplifierStr ? parseInt(amplifierStr, 10) : 0
|
|
1139
|
+
for (const entity of entities) {
|
|
1140
|
+
const current = this.effects.get(entity.id) ?? []
|
|
1141
|
+
current.push({ effect, duration: isNaN(duration) ? 30 : duration, amplifier: isNaN(amplifier) ? 0 : amplifier })
|
|
1142
|
+
this.effects.set(entity.id, current)
|
|
1143
|
+
}
|
|
1144
|
+
return entities.length > 0
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
private execXp(cmd: string, executor?: Entity): boolean {
|
|
1148
|
+
const match = cmd.match(/^xp (add|set) (\S+) (-?\d+)(?: (\S+))?$/)
|
|
1149
|
+
if (!match) return false
|
|
1150
|
+
|
|
1151
|
+
const [, action, target, amountStr] = match
|
|
1152
|
+
const amount = parseInt(amountStr, 10)
|
|
1153
|
+
const keys = this.resolveTargetKeys(target, executor)
|
|
1154
|
+
if (keys.length === 0) return false
|
|
1155
|
+
|
|
1156
|
+
for (const key of keys) {
|
|
1157
|
+
const current = this.xp.get(key) ?? 0
|
|
1158
|
+
this.xp.set(key, action === 'set' ? amount : current + amount)
|
|
1159
|
+
}
|
|
1160
|
+
return true
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// -------------------------------------------------------------------------
|
|
1164
|
+
// Summon Command
|
|
1165
|
+
// -------------------------------------------------------------------------
|
|
1166
|
+
|
|
1167
|
+
private execSummon(cmd: string): boolean {
|
|
1168
|
+
// summon minecraft:armor_stand <x> <y> <z> {Tags:["tag1","tag2"]}
|
|
1169
|
+
const match = cmd.match(/^summon (\S+) (\S+) (\S+) (\S+) ({.+})$/)
|
|
1170
|
+
if (match) {
|
|
1171
|
+
const [, type, x, y, z, nbtStr] = match
|
|
1172
|
+
const nbt = parseNBT(nbtStr)
|
|
1173
|
+
const position = this.parseAbsolutePosition(x, y, z) ?? { x: 0, y: 0, z: 0 }
|
|
1174
|
+
this.spawnEntity(nbt.Tags || [], type, position)
|
|
1175
|
+
return true
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Simple summon without NBT
|
|
1179
|
+
const simpleMatch = cmd.match(/^summon (\S+)(?: (\S+) (\S+) (\S+))?$/)
|
|
1180
|
+
if (simpleMatch) {
|
|
1181
|
+
const [, type, x, y, z] = simpleMatch
|
|
1182
|
+
const position = x && y && z
|
|
1183
|
+
? (this.parseAbsolutePosition(x, y, z) ?? { x: 0, y: 0, z: 0 })
|
|
1184
|
+
: { x: 0, y: 0, z: 0 }
|
|
1185
|
+
this.spawnEntity([], type, position)
|
|
1186
|
+
return true
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
return false
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// -------------------------------------------------------------------------
|
|
1193
|
+
// Return Command
|
|
1194
|
+
// -------------------------------------------------------------------------
|
|
1195
|
+
|
|
1196
|
+
private execReturn(cmd: string, executor?: Entity): boolean {
|
|
1197
|
+
const rest = cmd.slice(7).trim()
|
|
1198
|
+
|
|
1199
|
+
// return run <cmd>
|
|
1200
|
+
if (rest.startsWith('run ')) {
|
|
1201
|
+
const innerCmd = rest.slice(4)
|
|
1202
|
+
this.execCommand(innerCmd, executor)
|
|
1203
|
+
this.shouldReturn = true
|
|
1204
|
+
return true
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// return <value>
|
|
1208
|
+
const value = parseInt(rest, 10)
|
|
1209
|
+
if (!isNaN(value)) {
|
|
1210
|
+
this.returnValue = value
|
|
1211
|
+
this.shouldReturn = true
|
|
1212
|
+
return true
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return false
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// -------------------------------------------------------------------------
|
|
1219
|
+
// Scoreboard Helpers
|
|
1220
|
+
// -------------------------------------------------------------------------
|
|
1221
|
+
|
|
1222
|
+
getScore(player: string, objective: string): number {
|
|
1223
|
+
const obj = this.scoreboard.get(objective)
|
|
1224
|
+
if (!obj) return 0
|
|
1225
|
+
return obj.get(player) ?? 0
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
setScore(player: string, objective: string, value: number): void {
|
|
1229
|
+
let obj = this.scoreboard.get(objective)
|
|
1230
|
+
if (!obj) {
|
|
1231
|
+
obj = new Map()
|
|
1232
|
+
this.scoreboard.set(objective, obj)
|
|
1233
|
+
}
|
|
1234
|
+
obj.set(player, value)
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
addScore(player: string, objective: string, delta: number): void {
|
|
1238
|
+
const current = this.getScore(player, objective)
|
|
1239
|
+
this.setScore(player, objective, current + delta)
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// -------------------------------------------------------------------------
|
|
1243
|
+
// Storage Helpers
|
|
1244
|
+
// -------------------------------------------------------------------------
|
|
1245
|
+
|
|
1246
|
+
getStorage(path: string): any {
|
|
1247
|
+
// "ns:path.field" → parse namespace and nested fields
|
|
1248
|
+
const colonIdx = path.indexOf(':')
|
|
1249
|
+
if (colonIdx === -1) return this.storage.get(path)
|
|
1250
|
+
|
|
1251
|
+
const nsPath = path.slice(0, colonIdx + 1) + path.slice(colonIdx + 1).split('.')[0]
|
|
1252
|
+
const field = path.slice(colonIdx + 1).includes('.')
|
|
1253
|
+
? path.slice(path.indexOf('.', colonIdx) + 1)
|
|
1254
|
+
: undefined
|
|
1255
|
+
|
|
1256
|
+
if (!field) return this.storage.get(nsPath)
|
|
1257
|
+
return this.getStorageField(nsPath, field)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
setStorage(path: string, value: any): void {
|
|
1261
|
+
const colonIdx = path.indexOf(':')
|
|
1262
|
+
if (colonIdx === -1) {
|
|
1263
|
+
this.storage.set(path, value)
|
|
1264
|
+
return
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const basePath = path.slice(0, colonIdx + 1) + path.slice(colonIdx + 1).split('.')[0]
|
|
1268
|
+
const field = path.slice(colonIdx + 1).includes('.')
|
|
1269
|
+
? path.slice(path.indexOf('.', colonIdx) + 1)
|
|
1270
|
+
: undefined
|
|
1271
|
+
|
|
1272
|
+
if (!field) {
|
|
1273
|
+
this.storage.set(basePath, value)
|
|
1274
|
+
return
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
this.setStorageField(basePath, field, value)
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// -------------------------------------------------------------------------
|
|
1281
|
+
// Entity Helpers
|
|
1282
|
+
// -------------------------------------------------------------------------
|
|
1283
|
+
|
|
1284
|
+
spawnEntity(tags: string[], type: string = 'minecraft:armor_stand', position: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }): Entity {
|
|
1285
|
+
const id = `entity_${this.entityIdCounter++}`
|
|
1286
|
+
const entity: Entity = {
|
|
1287
|
+
id,
|
|
1288
|
+
tags: new Set(tags),
|
|
1289
|
+
scores: new Map(),
|
|
1290
|
+
selector: `@e[tag=${tags[0] ?? id},limit=1]`,
|
|
1291
|
+
type,
|
|
1292
|
+
position,
|
|
1293
|
+
}
|
|
1294
|
+
this.entities.push(entity)
|
|
1295
|
+
return entity
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
killEntity(tag: string): void {
|
|
1299
|
+
this.entities = this.entities.filter(e => !e.tags.has(tag))
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
getEntities(selector: string): Entity[] {
|
|
1303
|
+
return parseSelector(selector, this.entities)
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
private positionKey(x: string, y: string, z: string): string | null {
|
|
1307
|
+
const pos = this.parseAbsolutePosition(x, y, z)
|
|
1308
|
+
return pos ? `${pos.x},${pos.y},${pos.z}` : null
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
private parseAbsolutePosition(x: string, y: string, z: string): { x: number; y: number; z: number } | null {
|
|
1312
|
+
const coords = [x, y, z].map(coord => {
|
|
1313
|
+
if (coord.startsWith('~') || coord.startsWith('^')) {
|
|
1314
|
+
const offset = coord.slice(1)
|
|
1315
|
+
return offset === '' ? 0 : parseInt(offset, 10)
|
|
1316
|
+
}
|
|
1317
|
+
return parseInt(coord, 10)
|
|
1318
|
+
})
|
|
1319
|
+
if (coords.some(Number.isNaN)) return null
|
|
1320
|
+
return { x: coords[0], y: coords[1], z: coords[2] }
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
private resolvePosition(base: { x: number; y: number; z: number }, x: string, y: string, z: string): { x: number; y: number; z: number } | null {
|
|
1324
|
+
const values = [x, y, z].map((coord, index) => {
|
|
1325
|
+
if (coord.startsWith('~') || coord.startsWith('^')) {
|
|
1326
|
+
const offset = coord.slice(1)
|
|
1327
|
+
const delta = offset === '' ? 0 : parseInt(offset, 10)
|
|
1328
|
+
return [base.x, base.y, base.z][index] + delta
|
|
1329
|
+
}
|
|
1330
|
+
return parseInt(coord, 10)
|
|
1331
|
+
})
|
|
1332
|
+
if (values.some(Number.isNaN)) return null
|
|
1333
|
+
return { x: values[0], y: values[1], z: values[2] }
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
private parseTimeValue(value: string): number | null {
|
|
1337
|
+
if (/^-?\d+$/.test(value)) {
|
|
1338
|
+
return parseInt(value, 10)
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const aliases: Record<string, number> = {
|
|
1342
|
+
day: 1000,
|
|
1343
|
+
noon: 6000,
|
|
1344
|
+
night: 13000,
|
|
1345
|
+
midnight: 18000,
|
|
1346
|
+
sunrise: 23000,
|
|
1347
|
+
}
|
|
1348
|
+
return aliases[value] ?? null
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
private resolveTargetKeys(target: string, executor?: Entity): string[] {
|
|
1352
|
+
if (target.startsWith('@')) {
|
|
1353
|
+
const entities = target === '@s' && executor
|
|
1354
|
+
? [executor]
|
|
1355
|
+
: parseSelector(target, this.entities, executor)
|
|
1356
|
+
return entities.map(entity => entity.id)
|
|
1357
|
+
}
|
|
1358
|
+
return [target]
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// -------------------------------------------------------------------------
|
|
1362
|
+
// Output Helpers
|
|
1363
|
+
// -------------------------------------------------------------------------
|
|
1364
|
+
|
|
1365
|
+
getLastSaid(): string {
|
|
1366
|
+
return this.chatLog[this.chatLog.length - 1] ?? ''
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
getChatLog(): string[] {
|
|
1370
|
+
return [...this.chatLog]
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// -------------------------------------------------------------------------
|
|
1374
|
+
// Convenience: Compile and Load
|
|
1375
|
+
// -------------------------------------------------------------------------
|
|
1376
|
+
|
|
1377
|
+
compileAndLoad(source: string): void {
|
|
1378
|
+
const result = rsCompile(source, { namespace: this.namespace })
|
|
1379
|
+
if (!result.success || !result.files) {
|
|
1380
|
+
throw new Error('Compilation failed')
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Load all .mcfunction files
|
|
1384
|
+
for (const file of result.files) {
|
|
1385
|
+
if (file.path.endsWith('.mcfunction')) {
|
|
1386
|
+
// Extract function name from path
|
|
1387
|
+
// e.g., "data/test/function/increment.mcfunction" → "test:increment"
|
|
1388
|
+
const match = file.path.match(/data\/([^/]+)\/function\/(.+)\.mcfunction$/)
|
|
1389
|
+
if (match) {
|
|
1390
|
+
const [, ns, fnPath] = match
|
|
1391
|
+
const fnName = `${ns}:${fnPath.replace(/\//g, '/')}`
|
|
1392
|
+
this.loadFunction(fnName, file.content.split('\n'))
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Run load function
|
|
1398
|
+
this.load()
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Re-export for convenience
|
|
1403
|
+
export { parseRange, matchesRange, parseSelector }
|