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,367 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
|
|
3
|
+
interface BrigadierFile {
|
|
4
|
+
root: BrigadierNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface BrigadierNode {
|
|
8
|
+
type?: 'literal' | 'argument' | 'root'
|
|
9
|
+
name?: string
|
|
10
|
+
executable?: boolean
|
|
11
|
+
children?: BrigadierNode[]
|
|
12
|
+
redirects?: string[]
|
|
13
|
+
parser?: {
|
|
14
|
+
parser: string
|
|
15
|
+
modifier?: {
|
|
16
|
+
type?: string
|
|
17
|
+
} | null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ValidationResult {
|
|
22
|
+
valid: boolean
|
|
23
|
+
error?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const FUNCTION_ID_RE = /^[0-9a-z_.-]+:[0-9a-z_./-]+$/i
|
|
27
|
+
const INTEGER_RE = /^-?\d+$/
|
|
28
|
+
const SCORE_RANGE_RE = /^-?\d+\.\.$|^\.\.-?\d+$|^-?\d+\.\.-?\d+$|^-?\d+$/
|
|
29
|
+
const COMMENT_PREFIXES = [
|
|
30
|
+
'# RedScript runtime init',
|
|
31
|
+
'# block:',
|
|
32
|
+
'# RedScript tick dispatcher',
|
|
33
|
+
]
|
|
34
|
+
const SCOREBOARD_PLAYER_ACTIONS = new Set(['set', 'add', 'remove', 'get', 'operation', 'enable'])
|
|
35
|
+
const SCOREBOARD_OPERATIONS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '<', '>', '><'])
|
|
36
|
+
|
|
37
|
+
export class MCCommandValidator {
|
|
38
|
+
private readonly root: BrigadierNode
|
|
39
|
+
private readonly rootChildren: BrigadierNode[]
|
|
40
|
+
|
|
41
|
+
constructor(commandsPath: string) {
|
|
42
|
+
const parsed = JSON.parse(fs.readFileSync(commandsPath, 'utf-8')) as BrigadierFile
|
|
43
|
+
this.root = parsed.root
|
|
44
|
+
this.rootChildren = parsed.root.children ?? []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
validate(line: string): ValidationResult {
|
|
48
|
+
const trimmed = line.trim()
|
|
49
|
+
if (!trimmed || trimmed.startsWith('#') || COMMENT_PREFIXES.some(prefix => trimmed.startsWith(prefix))) {
|
|
50
|
+
return { valid: true }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tokens = tokenize(trimmed)
|
|
54
|
+
if (tokens.length === 0) {
|
|
55
|
+
return { valid: true }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!this.hasRootCommand(tokens[0])) {
|
|
59
|
+
return { valid: false, error: `Unknown root command: ${tokens[0]}` }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
switch (tokens[0]) {
|
|
63
|
+
case 'execute':
|
|
64
|
+
return this.validateExecute(tokens)
|
|
65
|
+
case 'scoreboard':
|
|
66
|
+
return this.validateScoreboard(tokens)
|
|
67
|
+
case 'function':
|
|
68
|
+
return this.validateFunction(tokens)
|
|
69
|
+
case 'data':
|
|
70
|
+
return this.validateData(tokens)
|
|
71
|
+
case 'return':
|
|
72
|
+
return this.validateReturn(tokens)
|
|
73
|
+
default:
|
|
74
|
+
return this.validateAgainstTree(tokens)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private hasRootCommand(command: string): boolean {
|
|
79
|
+
return this.rootChildren.some(child => child.type === 'literal' && child.name === command)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private validateExecute(tokens: string[]): ValidationResult {
|
|
83
|
+
const runIndex = tokens.indexOf('run')
|
|
84
|
+
if (runIndex === 1 || runIndex === tokens.length - 1) {
|
|
85
|
+
return { valid: false, error: 'Malformed execute run clause' }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (runIndex !== -1) {
|
|
89
|
+
const chainResult = this.validateAgainstTree(tokens.slice(0, runIndex))
|
|
90
|
+
if (!chainResult.valid) {
|
|
91
|
+
return chainResult
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return this.validate(tokens.slice(runIndex + 1).join(' '))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return this.validateAgainstTree(tokens)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private validateScoreboard(tokens: string[]): ValidationResult {
|
|
101
|
+
if (tokens[1] === 'objectives' && tokens[2] === 'add') {
|
|
102
|
+
if (tokens.length < 5) {
|
|
103
|
+
return { valid: false, error: 'scoreboard objectives add requires name and criteria' }
|
|
104
|
+
}
|
|
105
|
+
return this.validateAgainstTree(tokens)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (tokens[1] !== 'players' || !SCOREBOARD_PLAYER_ACTIONS.has(tokens[2] ?? '')) {
|
|
109
|
+
return this.validateAgainstTree(tokens)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const action = tokens[2]
|
|
113
|
+
if (action === 'enable') {
|
|
114
|
+
if (tokens.length !== 5) {
|
|
115
|
+
return { valid: false, error: 'scoreboard players enable requires target and objective' }
|
|
116
|
+
}
|
|
117
|
+
return this.validateAgainstTree(tokens)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (action === 'get') {
|
|
121
|
+
if (tokens.length !== 5) {
|
|
122
|
+
return { valid: false, error: 'scoreboard players get requires target and objective' }
|
|
123
|
+
}
|
|
124
|
+
return this.validateAgainstTree(tokens)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (action === 'operation') {
|
|
128
|
+
if (tokens.length !== 8) {
|
|
129
|
+
return { valid: false, error: 'scoreboard players operation requires 5 operands' }
|
|
130
|
+
}
|
|
131
|
+
if (!SCOREBOARD_OPERATIONS.has(tokens[5])) {
|
|
132
|
+
return { valid: false, error: `Unknown scoreboard operation: ${tokens[5]}` }
|
|
133
|
+
}
|
|
134
|
+
return this.validateAgainstTree(tokens)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (tokens.length !== 6) {
|
|
138
|
+
return { valid: false, error: `scoreboard players ${action} requires target, objective, and value` }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!INTEGER_RE.test(tokens[5])) {
|
|
142
|
+
return { valid: false, error: `Expected integer value, got: ${tokens[5]}` }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return this.validateAgainstTree(tokens)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private validateFunction(tokens: string[]): ValidationResult {
|
|
149
|
+
if (tokens.length !== 2 || !FUNCTION_ID_RE.test(tokens[1])) {
|
|
150
|
+
return { valid: false, error: 'function requires a namespaced function id' }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return this.validateAgainstTree(tokens)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private validateData(tokens: string[]): ValidationResult {
|
|
157
|
+
if (tokens.length < 5) {
|
|
158
|
+
return { valid: false, error: 'data command is incomplete' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const action = tokens[1]
|
|
162
|
+
if (!['get', 'modify', 'merge', 'remove'].includes(action)) {
|
|
163
|
+
return this.validateAgainstTree(tokens)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const targetType = tokens[2]
|
|
167
|
+
if (!['storage', 'entity', 'block'].includes(targetType)) {
|
|
168
|
+
return { valid: false, error: `Unsupported data target: ${targetType}` }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (action === 'get') {
|
|
172
|
+
if (tokens.length < 5) {
|
|
173
|
+
return { valid: false, error: 'data get requires target and path' }
|
|
174
|
+
}
|
|
175
|
+
if (tokens[5] && !isNumberish(tokens[5])) {
|
|
176
|
+
return { valid: false, error: `Invalid data get scale: ${tokens[5]}` }
|
|
177
|
+
}
|
|
178
|
+
return this.validateAgainstTree(tokens)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (action === 'modify') {
|
|
182
|
+
if (tokens.length < 7) {
|
|
183
|
+
return { valid: false, error: 'data modify is incomplete' }
|
|
184
|
+
}
|
|
185
|
+
if (!['set', 'append', 'prepend', 'insert', 'merge'].includes(tokens[5])) {
|
|
186
|
+
return { valid: false, error: `Unsupported data modify mode: ${tokens[5]}` }
|
|
187
|
+
}
|
|
188
|
+
return this.validateAgainstTree(tokens)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return this.validateAgainstTree(tokens)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private validateReturn(tokens: string[]): ValidationResult {
|
|
195
|
+
if (tokens.length < 2) {
|
|
196
|
+
return { valid: false, error: 'return requires a value or run clause' }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (tokens[1] === 'run') {
|
|
200
|
+
if (tokens.length < 3) {
|
|
201
|
+
return { valid: false, error: 'return run requires an inner command' }
|
|
202
|
+
}
|
|
203
|
+
return this.validate(tokens.slice(2).join(' '))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!INTEGER_RE.test(tokens[1])) {
|
|
207
|
+
return { valid: false, error: `Invalid return value: ${tokens[1]}` }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return this.validateAgainstTree(tokens)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private validateAgainstTree(tokens: string[]): ValidationResult {
|
|
214
|
+
const memo = new Map<string, boolean>()
|
|
215
|
+
const isValid = walk(this.root, tokens, 0, memo, this.rootChildren)
|
|
216
|
+
|
|
217
|
+
return isValid
|
|
218
|
+
? { valid: true }
|
|
219
|
+
: { valid: false, error: `Command does not match Brigadier tree: ${tokens.join(' ')}` }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function walk(
|
|
224
|
+
node: BrigadierNode,
|
|
225
|
+
tokens: string[],
|
|
226
|
+
index: number,
|
|
227
|
+
memo: Map<string, boolean>,
|
|
228
|
+
rootChildren: BrigadierNode[]
|
|
229
|
+
): boolean {
|
|
230
|
+
const key = `${node.name ?? '<root>'}:${index}`
|
|
231
|
+
const cached = memo.get(key)
|
|
232
|
+
if (cached !== undefined) {
|
|
233
|
+
return cached
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (index === tokens.length) {
|
|
237
|
+
const done = node.executable === true || (node.children ?? []).length === 0
|
|
238
|
+
memo.set(key, done)
|
|
239
|
+
return done
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const children = node.children ?? []
|
|
243
|
+
for (const child of children) {
|
|
244
|
+
if (child.type === 'literal') {
|
|
245
|
+
if (child.name === tokens[index] && walk(child, tokens, index + 1, memo, rootChildren)) {
|
|
246
|
+
memo.set(key, true)
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (child.type !== 'argument') {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const parser = child.parser?.parser
|
|
257
|
+
const modifier = child.parser?.modifier?.type
|
|
258
|
+
if (parserConsumesRest(parser, modifier)) {
|
|
259
|
+
const done = child.executable === true || (child.children ?? []).length === 0
|
|
260
|
+
if (done) {
|
|
261
|
+
memo.set(key, true)
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const width = parserTokenWidth(parser, tokens, index)
|
|
267
|
+
if (width === null) {
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const nextIndex = index + width
|
|
272
|
+
if (walk(child, tokens, nextIndex, memo, rootChildren)) {
|
|
273
|
+
memo.set(key, true)
|
|
274
|
+
return true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const redirect of child.redirects ?? []) {
|
|
278
|
+
const target = rootChildren.find(candidate => candidate.name === redirect)
|
|
279
|
+
if (target && walk(target, tokens, nextIndex, memo, rootChildren)) {
|
|
280
|
+
memo.set(key, true)
|
|
281
|
+
return true
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
memo.set(key, false)
|
|
287
|
+
return false
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function parserConsumesRest(parser?: string, modifier?: string): boolean {
|
|
291
|
+
return (
|
|
292
|
+
(parser === 'brigadier:string' && modifier === 'greedy') ||
|
|
293
|
+
parser === 'minecraft:message'
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parserTokenWidth(parser: string | undefined, tokens: string[], index: number): number | null {
|
|
298
|
+
switch (parser) {
|
|
299
|
+
case 'minecraft:vec3':
|
|
300
|
+
case 'minecraft:block_pos':
|
|
301
|
+
return index + 3 <= tokens.length ? 3 : null
|
|
302
|
+
case 'minecraft:vec2':
|
|
303
|
+
case 'minecraft:column_pos':
|
|
304
|
+
case 'minecraft:rotation':
|
|
305
|
+
return index + 2 <= tokens.length ? 2 : null
|
|
306
|
+
default:
|
|
307
|
+
return index < tokens.length ? 1 : null
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function tokenize(line: string): string[] {
|
|
312
|
+
const tokens: string[] = []
|
|
313
|
+
let current = ''
|
|
314
|
+
let quote: '"' | '\'' | null = null
|
|
315
|
+
let escape = false
|
|
316
|
+
let bracketDepth = 0
|
|
317
|
+
let braceDepth = 0
|
|
318
|
+
|
|
319
|
+
for (const char of line) {
|
|
320
|
+
if (escape) {
|
|
321
|
+
current += char
|
|
322
|
+
escape = false
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (quote) {
|
|
327
|
+
current += char
|
|
328
|
+
if (char === '\\') {
|
|
329
|
+
escape = true
|
|
330
|
+
} else if (char === quote) {
|
|
331
|
+
quote = null
|
|
332
|
+
}
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (char === '"' || char === '\'') {
|
|
337
|
+
quote = char
|
|
338
|
+
current += char
|
|
339
|
+
continue
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (char === '[') bracketDepth += 1
|
|
343
|
+
if (char === ']') bracketDepth = Math.max(0, bracketDepth - 1)
|
|
344
|
+
if (char === '{') braceDepth += 1
|
|
345
|
+
if (char === '}') braceDepth = Math.max(0, braceDepth - 1)
|
|
346
|
+
|
|
347
|
+
if (/\s/.test(char) && bracketDepth === 0 && braceDepth === 0) {
|
|
348
|
+
if (current) {
|
|
349
|
+
tokens.push(current)
|
|
350
|
+
current = ''
|
|
351
|
+
}
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
current += char
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (current) {
|
|
359
|
+
tokens.push(current)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return tokens
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isNumberish(value: string): boolean {
|
|
366
|
+
return /^-?\d+(\.\d+)?$/.test(value) || SCORE_RANGE_RE.test(value)
|
|
367
|
+
}
|
package/src/nbt/index.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
export const enum TagType {
|
|
2
|
+
End = 0,
|
|
3
|
+
Byte = 1,
|
|
4
|
+
Short = 2,
|
|
5
|
+
Int = 3,
|
|
6
|
+
Long = 4,
|
|
7
|
+
Float = 5,
|
|
8
|
+
Double = 6,
|
|
9
|
+
ByteArray = 7,
|
|
10
|
+
String = 8,
|
|
11
|
+
List = 9,
|
|
12
|
+
Compound = 10,
|
|
13
|
+
IntArray = 11,
|
|
14
|
+
LongArray = 12,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type EndTag = { type: TagType.End }
|
|
18
|
+
export type ByteTag = { type: TagType.Byte; value: number }
|
|
19
|
+
export type ShortTag = { type: TagType.Short; value: number }
|
|
20
|
+
export type IntTag = { type: TagType.Int; value: number }
|
|
21
|
+
export type LongTag = { type: TagType.Long; value: bigint }
|
|
22
|
+
export type FloatTag = { type: TagType.Float; value: number }
|
|
23
|
+
export type DoubleTag = { type: TagType.Double; value: number }
|
|
24
|
+
export type ByteArrayTag = { type: TagType.ByteArray; value: Int8Array }
|
|
25
|
+
export type StringTag = { type: TagType.String; value: string }
|
|
26
|
+
export type ListTag = { type: TagType.List; elementType: TagType; items: NbtTag[] }
|
|
27
|
+
export type CompoundTag = { type: TagType.Compound; entries: Map<string, NbtTag> }
|
|
28
|
+
export type IntArrayTag = { type: TagType.IntArray; value: Int32Array }
|
|
29
|
+
export type LongArrayTag = { type: TagType.LongArray; value: BigInt64Array }
|
|
30
|
+
|
|
31
|
+
export type NbtTag =
|
|
32
|
+
| EndTag
|
|
33
|
+
| ByteTag
|
|
34
|
+
| ShortTag
|
|
35
|
+
| IntTag
|
|
36
|
+
| LongTag
|
|
37
|
+
| FloatTag
|
|
38
|
+
| DoubleTag
|
|
39
|
+
| ByteArrayTag
|
|
40
|
+
| StringTag
|
|
41
|
+
| ListTag
|
|
42
|
+
| CompoundTag
|
|
43
|
+
| IntArrayTag
|
|
44
|
+
| LongArrayTag
|
|
45
|
+
|
|
46
|
+
function encodeModifiedUtf8(value: string): Buffer {
|
|
47
|
+
const bytes: number[] = []
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < value.length; i++) {
|
|
50
|
+
const codeUnit = value.charCodeAt(i)
|
|
51
|
+
|
|
52
|
+
if (codeUnit !== 0 && codeUnit <= 0x7f) {
|
|
53
|
+
bytes.push(codeUnit)
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (codeUnit <= 0x07ff) {
|
|
58
|
+
bytes.push(
|
|
59
|
+
0xc0 | ((codeUnit >> 6) & 0x1f),
|
|
60
|
+
0x80 | (codeUnit & 0x3f)
|
|
61
|
+
)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
bytes.push(
|
|
66
|
+
0xe0 | ((codeUnit >> 12) & 0x0f),
|
|
67
|
+
0x80 | ((codeUnit >> 6) & 0x3f),
|
|
68
|
+
0x80 | (codeUnit & 0x3f)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (bytes.length > 0xffff) {
|
|
73
|
+
throw new Error(`NBT string is too long: ${bytes.length} bytes`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const buffer = Buffer.allocUnsafe(2 + bytes.length)
|
|
77
|
+
buffer.writeUInt16BE(bytes.length, 0)
|
|
78
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
79
|
+
buffer[2 + i] = bytes[i]
|
|
80
|
+
}
|
|
81
|
+
return buffer
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function decodeModifiedUtf8(buffer: Buffer, offset: number): { value: string; offset: number } {
|
|
85
|
+
const byteLength = buffer.readUInt16BE(offset)
|
|
86
|
+
offset += 2
|
|
87
|
+
|
|
88
|
+
const codeUnits: number[] = []
|
|
89
|
+
const end = offset + byteLength
|
|
90
|
+
|
|
91
|
+
while (offset < end) {
|
|
92
|
+
const first = buffer[offset++]
|
|
93
|
+
|
|
94
|
+
if ((first & 0x80) === 0) {
|
|
95
|
+
codeUnits.push(first)
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if ((first & 0xe0) === 0xc0) {
|
|
100
|
+
const second = buffer[offset++]
|
|
101
|
+
codeUnits.push(((first & 0x1f) << 6) | (second & 0x3f))
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const second = buffer[offset++]
|
|
106
|
+
const third = buffer[offset++]
|
|
107
|
+
codeUnits.push(
|
|
108
|
+
((first & 0x0f) << 12) |
|
|
109
|
+
((second & 0x3f) << 6) |
|
|
110
|
+
(third & 0x3f)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
value: String.fromCharCode(...codeUnits),
|
|
116
|
+
offset,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writePayload(tag: NbtTag): Buffer {
|
|
121
|
+
switch (tag.type) {
|
|
122
|
+
case TagType.End:
|
|
123
|
+
return Buffer.alloc(0)
|
|
124
|
+
case TagType.Byte: {
|
|
125
|
+
const buffer = Buffer.allocUnsafe(1)
|
|
126
|
+
buffer.writeInt8(tag.value, 0)
|
|
127
|
+
return buffer
|
|
128
|
+
}
|
|
129
|
+
case TagType.Short: {
|
|
130
|
+
const buffer = Buffer.allocUnsafe(2)
|
|
131
|
+
buffer.writeInt16BE(tag.value, 0)
|
|
132
|
+
return buffer
|
|
133
|
+
}
|
|
134
|
+
case TagType.Int: {
|
|
135
|
+
const buffer = Buffer.allocUnsafe(4)
|
|
136
|
+
buffer.writeInt32BE(tag.value, 0)
|
|
137
|
+
return buffer
|
|
138
|
+
}
|
|
139
|
+
case TagType.Long: {
|
|
140
|
+
const buffer = Buffer.allocUnsafe(8)
|
|
141
|
+
buffer.writeBigInt64BE(tag.value, 0)
|
|
142
|
+
return buffer
|
|
143
|
+
}
|
|
144
|
+
case TagType.Float: {
|
|
145
|
+
const buffer = Buffer.allocUnsafe(4)
|
|
146
|
+
buffer.writeFloatBE(tag.value, 0)
|
|
147
|
+
return buffer
|
|
148
|
+
}
|
|
149
|
+
case TagType.Double: {
|
|
150
|
+
const buffer = Buffer.allocUnsafe(8)
|
|
151
|
+
buffer.writeDoubleBE(tag.value, 0)
|
|
152
|
+
return buffer
|
|
153
|
+
}
|
|
154
|
+
case TagType.ByteArray: {
|
|
155
|
+
const header = Buffer.allocUnsafe(4)
|
|
156
|
+
header.writeInt32BE(tag.value.length, 0)
|
|
157
|
+
return Buffer.concat([header, Buffer.from(tag.value)])
|
|
158
|
+
}
|
|
159
|
+
case TagType.String:
|
|
160
|
+
return encodeModifiedUtf8(tag.value)
|
|
161
|
+
case TagType.List: {
|
|
162
|
+
const header = Buffer.allocUnsafe(5)
|
|
163
|
+
header.writeUInt8(tag.elementType, 0)
|
|
164
|
+
header.writeInt32BE(tag.items.length, 1)
|
|
165
|
+
return Buffer.concat([header, ...tag.items.map(writePayload)])
|
|
166
|
+
}
|
|
167
|
+
case TagType.Compound: {
|
|
168
|
+
const parts: Buffer[] = []
|
|
169
|
+
for (const [name, entry] of tag.entries) {
|
|
170
|
+
parts.push(writeNamedTag(entry, name))
|
|
171
|
+
}
|
|
172
|
+
parts.push(Buffer.from([TagType.End]))
|
|
173
|
+
return Buffer.concat(parts)
|
|
174
|
+
}
|
|
175
|
+
case TagType.IntArray: {
|
|
176
|
+
const header = Buffer.allocUnsafe(4 + tag.value.length * 4)
|
|
177
|
+
header.writeInt32BE(tag.value.length, 0)
|
|
178
|
+
for (let i = 0; i < tag.value.length; i++) {
|
|
179
|
+
header.writeInt32BE(tag.value[i], 4 + i * 4)
|
|
180
|
+
}
|
|
181
|
+
return header
|
|
182
|
+
}
|
|
183
|
+
case TagType.LongArray: {
|
|
184
|
+
const header = Buffer.allocUnsafe(4 + tag.value.length * 8)
|
|
185
|
+
header.writeInt32BE(tag.value.length, 0)
|
|
186
|
+
for (let i = 0; i < tag.value.length; i++) {
|
|
187
|
+
header.writeBigInt64BE(tag.value[i], 4 + i * 8)
|
|
188
|
+
}
|
|
189
|
+
return header
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function writeNamedTag(tag: NbtTag, name: string): Buffer {
|
|
195
|
+
if (tag.type === TagType.End) {
|
|
196
|
+
throw new Error('TAG_End cannot be written as a named tag')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const nameBuffer = encodeModifiedUtf8(name)
|
|
200
|
+
return Buffer.concat([
|
|
201
|
+
Buffer.from([tag.type]),
|
|
202
|
+
nameBuffer,
|
|
203
|
+
writePayload(tag),
|
|
204
|
+
])
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readPayload(type: TagType, buffer: Buffer, offset: number): { tag: NbtTag; offset: number } {
|
|
208
|
+
switch (type) {
|
|
209
|
+
case TagType.End:
|
|
210
|
+
return { tag: { type: TagType.End }, offset }
|
|
211
|
+
case TagType.Byte:
|
|
212
|
+
return { tag: { type: type, value: buffer.readInt8(offset) }, offset: offset + 1 }
|
|
213
|
+
case TagType.Short:
|
|
214
|
+
return { tag: { type: type, value: buffer.readInt16BE(offset) }, offset: offset + 2 }
|
|
215
|
+
case TagType.Int:
|
|
216
|
+
return { tag: { type: type, value: buffer.readInt32BE(offset) }, offset: offset + 4 }
|
|
217
|
+
case TagType.Long:
|
|
218
|
+
return { tag: { type: type, value: buffer.readBigInt64BE(offset) }, offset: offset + 8 }
|
|
219
|
+
case TagType.Float:
|
|
220
|
+
return { tag: { type: type, value: buffer.readFloatBE(offset) }, offset: offset + 4 }
|
|
221
|
+
case TagType.Double:
|
|
222
|
+
return { tag: { type: type, value: buffer.readDoubleBE(offset) }, offset: offset + 8 }
|
|
223
|
+
case TagType.ByteArray: {
|
|
224
|
+
const length = buffer.readInt32BE(offset)
|
|
225
|
+
offset += 4
|
|
226
|
+
const value = new Int8Array(length)
|
|
227
|
+
for (let i = 0; i < length; i++) {
|
|
228
|
+
value[i] = buffer.readInt8(offset + i)
|
|
229
|
+
}
|
|
230
|
+
return { tag: { type, value }, offset: offset + length }
|
|
231
|
+
}
|
|
232
|
+
case TagType.String: {
|
|
233
|
+
const decoded = decodeModifiedUtf8(buffer, offset)
|
|
234
|
+
return { tag: { type, value: decoded.value }, offset: decoded.offset }
|
|
235
|
+
}
|
|
236
|
+
case TagType.List: {
|
|
237
|
+
const elementType = buffer.readUInt8(offset) as TagType
|
|
238
|
+
const length = buffer.readInt32BE(offset + 1)
|
|
239
|
+
offset += 5
|
|
240
|
+
const items: NbtTag[] = []
|
|
241
|
+
for (let i = 0; i < length; i++) {
|
|
242
|
+
const parsed = readPayload(elementType, buffer, offset)
|
|
243
|
+
items.push(parsed.tag)
|
|
244
|
+
offset = parsed.offset
|
|
245
|
+
}
|
|
246
|
+
return { tag: { type, elementType, items }, offset }
|
|
247
|
+
}
|
|
248
|
+
case TagType.Compound: {
|
|
249
|
+
const entries = new Map<string, NbtTag>()
|
|
250
|
+
while (true) {
|
|
251
|
+
const entryType = buffer.readUInt8(offset) as TagType
|
|
252
|
+
offset += 1
|
|
253
|
+
if (entryType === TagType.End) break
|
|
254
|
+
const name = decodeModifiedUtf8(buffer, offset)
|
|
255
|
+
offset = name.offset
|
|
256
|
+
const parsed = readPayload(entryType, buffer, offset)
|
|
257
|
+
entries.set(name.value, parsed.tag)
|
|
258
|
+
offset = parsed.offset
|
|
259
|
+
}
|
|
260
|
+
return { tag: { type, entries }, offset }
|
|
261
|
+
}
|
|
262
|
+
case TagType.IntArray: {
|
|
263
|
+
const length = buffer.readInt32BE(offset)
|
|
264
|
+
offset += 4
|
|
265
|
+
const value = new Int32Array(length)
|
|
266
|
+
for (let i = 0; i < length; i++) {
|
|
267
|
+
value[i] = buffer.readInt32BE(offset + i * 4)
|
|
268
|
+
}
|
|
269
|
+
return { tag: { type, value }, offset: offset + length * 4 }
|
|
270
|
+
}
|
|
271
|
+
case TagType.LongArray: {
|
|
272
|
+
const length = buffer.readInt32BE(offset)
|
|
273
|
+
offset += 4
|
|
274
|
+
const value = new BigInt64Array(length)
|
|
275
|
+
for (let i = 0; i < length; i++) {
|
|
276
|
+
value[i] = buffer.readBigInt64BE(offset + i * 8)
|
|
277
|
+
}
|
|
278
|
+
return { tag: { type, value }, offset: offset + length * 8 }
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
throw new Error(`Unsupported NBT tag type: ${type}`)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function writeNbt(tag: NbtTag, name: string): Buffer {
|
|
286
|
+
return writeNamedTag(tag, name)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function readNbt(buffer: Buffer): { name: string; tag: NbtTag } {
|
|
290
|
+
let offset = 0
|
|
291
|
+
const type = buffer.readUInt8(offset) as TagType
|
|
292
|
+
offset += 1
|
|
293
|
+
|
|
294
|
+
if (type === TagType.End) {
|
|
295
|
+
throw new Error('Invalid root tag: TAG_End')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const decodedName = decodeModifiedUtf8(buffer, offset)
|
|
299
|
+
offset = decodedName.offset
|
|
300
|
+
const parsed = readPayload(type, buffer, offset)
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
name: decodedName.value,
|
|
304
|
+
tag: parsed.tag,
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export const nbt = {
|
|
309
|
+
byte: (value: number): ByteTag => ({ type: TagType.Byte, value }),
|
|
310
|
+
short: (value: number): ShortTag => ({ type: TagType.Short, value }),
|
|
311
|
+
int: (value: number): IntTag => ({ type: TagType.Int, value }),
|
|
312
|
+
long: (value: bigint): LongTag => ({ type: TagType.Long, value }),
|
|
313
|
+
float: (value: number): FloatTag => ({ type: TagType.Float, value }),
|
|
314
|
+
double: (value: number): DoubleTag => ({ type: TagType.Double, value }),
|
|
315
|
+
string: (value: string): StringTag => ({ type: TagType.String, value }),
|
|
316
|
+
list: (elementType: TagType, items: NbtTag[]): ListTag => ({ type: TagType.List, elementType, items }),
|
|
317
|
+
compound: (entries: Record<string, NbtTag>): CompoundTag =>
|
|
318
|
+
({ type: TagType.Compound, entries: new Map(Object.entries(entries)) }),
|
|
319
|
+
intArray: (values: number[]): IntArrayTag => ({ type: TagType.IntArray, value: Int32Array.from(values) }),
|
|
320
|
+
byteArray: (values: number[]): ByteArrayTag => ({ type: TagType.ByteArray, value: Int8Array.from(values) }),
|
|
321
|
+
}
|