redscript-mc 1.0.0 → 1.2.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.yml +72 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
- package/CHANGELOG.md +112 -0
- package/CONTRIBUTING.md +140 -0
- package/README.md +28 -19
- package/README.zh.md +28 -19
- package/dist/__tests__/cli.test.js +148 -10
- package/dist/__tests__/codegen.test.js +26 -1
- package/dist/__tests__/diagnostics.test.js +5 -5
- package/dist/__tests__/e2e.test.js +336 -17
- package/dist/__tests__/formatter.test.d.ts +1 -0
- package/dist/__tests__/formatter.test.js +40 -0
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +200 -12
- package/dist/__tests__/mc-integration.test.js +370 -31
- package/dist/__tests__/mc-syntax.test.js +3 -3
- package/dist/__tests__/nbt.test.js +2 -2
- package/dist/__tests__/optimizer-advanced.test.js +5 -5
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +9 -9
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +40 -3
- package/dist/cli.js +25 -7
- package/dist/codegen/mcfunction/index.d.ts +1 -1
- package/dist/codegen/mcfunction/index.js +38 -3
- package/dist/codegen/structure/index.js +32 -1
- package/dist/compile.d.ts +10 -0
- package/dist/compile.js +36 -5
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/formatter/index.d.ts +1 -0
- package/dist/formatter/index.js +26 -0
- package/dist/index.js +3 -2
- package/dist/ir/builder.d.ts +2 -1
- package/dist/ir/types.d.ts +11 -2
- package/dist/ir/types.js +1 -1
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +34 -1
- package/dist/lowering/index.js +622 -23
- package/dist/mc-test/runner.d.ts +2 -2
- package/dist/mc-test/runner.js +3 -3
- package/dist/mc-test/setup.js +2 -2
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +153 -16
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/COMPILATION_STATS.md +24 -24
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/docs/IMPLEMENTATION_GUIDE.md +1 -1
- package/docs/STRUCTURE_TARGET.md +1 -1
- package/editors/vscode/.vscodeignore +1 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icons/mcrs.svg +7 -0
- package/editors/vscode/icons/redscript-icons.json +10 -0
- package/editors/vscode/out/extension.js +1295 -80
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +10 -3
- package/editors/vscode/src/hover.ts +55 -2
- package/editors/vscode/src/symbols.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +176 -10
- package/src/__tests__/codegen.test.ts +28 -1
- package/src/__tests__/diagnostics.test.ts +5 -5
- package/src/__tests__/e2e.test.ts +335 -17
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +14 -2
- package/src/__tests__/lowering.test.ts +226 -12
- package/src/__tests__/mc-integration.test.ts +421 -31
- package/src/__tests__/mc-syntax.test.ts +3 -3
- package/src/__tests__/nbt.test.ts +2 -2
- package/src/__tests__/optimizer-advanced.test.ts +5 -5
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +9 -9
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +44 -3
- package/src/cli.ts +10 -10
- package/src/codegen/mcfunction/index.ts +40 -3
- package/src/codegen/structure/index.ts +35 -1
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- package/src/examples/capture_the_flag.mcrs +208 -0
- package/src/examples/{counter.rs → counter.mcrs} +1 -1
- package/src/examples/hunger_games.mcrs +301 -0
- package/src/examples/new_features_demo.mcrs +193 -0
- package/src/examples/parkour_race.mcrs +233 -0
- package/src/examples/rpg.mcrs +13 -0
- package/src/examples/{shop.rs → shop.mcrs} +1 -1
- package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
- package/src/examples/{turret.rs → turret.mcrs} +1 -1
- package/src/examples/zombie_survival.mcrs +314 -0
- package/src/index.ts +4 -3
- package/src/ir/builder.ts +3 -1
- package/src/ir/types.ts +12 -2
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +684 -24
- package/src/mc-test/runner.ts +3 -3
- package/src/mc-test/setup.ts +2 -2
- package/src/parser/index.ts +170 -19
- package/src/stdlib/README.md +178 -140
- package/src/stdlib/bossbar.mcrs +68 -0
- package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
- package/src/stdlib/effects.mcrs +64 -0
- package/src/stdlib/interactions.mcrs +195 -0
- package/src/stdlib/inventory.mcrs +38 -0
- package/src/stdlib/mobs.mcrs +99 -0
- package/src/stdlib/particles.mcrs +52 -0
- package/src/stdlib/sets.mcrs +20 -0
- package/src/stdlib/spawn.mcrs +41 -0
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/teams.mcrs +68 -0
- package/src/stdlib/timer.mcrs +72 -0
- package/src/stdlib/world.mcrs +92 -0
- package/src/typechecker/index.ts +404 -18
- package/src/examples/rpg.rs +0 -13
- package/src/stdlib/mobs.rs +0 -99
- package/src/stdlib/timer.rs +0 -51
- /package/src/examples/{arena.rs → arena.mcrs} +0 -0
- /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
- /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
- /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
- /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
- /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
- /package/src/stdlib/{math.rs → math.mcrs} +0 -0
- /package/src/stdlib/{player.rs → player.mcrs} +0 -0
- /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
- /package/src/templates/{combat.rs → combat.mcrs} +0 -0
- /package/src/templates/{economy.rs → economy.mcrs} +0 -0
- /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
- /package/src/templates/{quest.rs → quest.mcrs} +0 -0
- /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "redscript-vscode",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "1.0.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"redscript": "file:../../"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "redscript-vscode",
|
|
3
3
|
"displayName": "RedScript for Minecraft",
|
|
4
4
|
"description": "Syntax highlighting, error diagnostics, and language support for RedScript — a compiler targeting Minecraft Java Edition",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "1.0.0",
|
|
6
6
|
"publisher": "bkmashiro",
|
|
7
7
|
"icon": "icon.png",
|
|
8
8
|
"license": "MIT",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"redscript"
|
|
37
37
|
],
|
|
38
38
|
"extensions": [
|
|
39
|
-
".
|
|
39
|
+
".mcrs"
|
|
40
40
|
],
|
|
41
41
|
"configuration": "./redscript-language-configuration.json"
|
|
42
42
|
},
|
|
@@ -116,7 +116,14 @@
|
|
|
116
116
|
}
|
|
117
117
|
]
|
|
118
118
|
}
|
|
119
|
-
}
|
|
119
|
+
},
|
|
120
|
+
"iconThemes": [
|
|
121
|
+
{
|
|
122
|
+
"id": "redscript-icons",
|
|
123
|
+
"label": "RedScript Icons",
|
|
124
|
+
"path": "./icons/redscript-icons.json"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
120
127
|
},
|
|
121
128
|
"scripts": {
|
|
122
129
|
"compile": "node build.mjs",
|
|
@@ -766,17 +766,70 @@ function findFnDeclLine(document: vscode.TextDocument, name: string): number | n
|
|
|
766
766
|
function findFnSignature(document: vscode.TextDocument, name: string): string | null {
|
|
767
767
|
const text = document.getText()
|
|
768
768
|
// Match: fn name(params) or fn name(params) -> Type
|
|
769
|
-
const re = new RegExp(`\\bfn\\s+${escapeRe(name)}\\s*\\(([^)]*)\\)(?:\\s*->\\s*([A-Za-z_][A-Za-z0-9_\\[\\]]*))
|
|
769
|
+
const re = new RegExp(`\\bfn\\s+${escapeRe(name)}\\s*\\(([^)]*)\\)(?:\\s*->\\s*([A-Za-z_][A-Za-z0-9_\\[\\]]*))?\\s*\\{`, 'm')
|
|
770
770
|
const match = re.exec(text)
|
|
771
771
|
if (!match) return null
|
|
772
772
|
const params = match[1].trim()
|
|
773
|
-
|
|
773
|
+
let returnType = match[2]
|
|
774
|
+
|
|
775
|
+
// If no explicit return type, try to infer from return statements
|
|
776
|
+
if (!returnType) {
|
|
777
|
+
returnType = inferReturnType(text, match.index + match[0].length)
|
|
778
|
+
}
|
|
779
|
+
|
|
774
780
|
if (returnType) {
|
|
775
781
|
return `fn ${name}(${params}) -> ${returnType}`
|
|
776
782
|
}
|
|
777
783
|
return `fn ${name}(${params})`
|
|
778
784
|
}
|
|
779
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Infer return type by looking at return statements in function body
|
|
788
|
+
*/
|
|
789
|
+
function inferReturnType(text: string, bodyStart: number): string | null {
|
|
790
|
+
// Find the matching closing brace
|
|
791
|
+
let braceCount = 1
|
|
792
|
+
let pos = bodyStart
|
|
793
|
+
while (pos < text.length && braceCount > 0) {
|
|
794
|
+
if (text[pos] === '{') braceCount++
|
|
795
|
+
else if (text[pos] === '}') braceCount--
|
|
796
|
+
pos++
|
|
797
|
+
}
|
|
798
|
+
const body = text.slice(bodyStart, pos - 1)
|
|
799
|
+
|
|
800
|
+
// Look for return statements
|
|
801
|
+
const returnMatch = body.match(/\breturn\s+(.+?);/)
|
|
802
|
+
if (!returnMatch) return null
|
|
803
|
+
|
|
804
|
+
const returnExpr = returnMatch[1].trim()
|
|
805
|
+
|
|
806
|
+
// Infer type from expression
|
|
807
|
+
if (/^\d+$/.test(returnExpr)) return 'int'
|
|
808
|
+
if (/^\d+\.\d+$/.test(returnExpr)) return 'float'
|
|
809
|
+
if (/^\d+[bB]$/.test(returnExpr)) return 'byte'
|
|
810
|
+
if (/^\d+[sS]$/.test(returnExpr)) return 'short'
|
|
811
|
+
if (/^\d+[lL]$/.test(returnExpr)) return 'long'
|
|
812
|
+
if (/^\d+(\.\d+)?[dD]$/.test(returnExpr)) return 'double'
|
|
813
|
+
if (/^".*"$/.test(returnExpr)) return 'string'
|
|
814
|
+
if (/^(true|false)$/.test(returnExpr)) return 'bool'
|
|
815
|
+
if (/^@[aeprs]/.test(returnExpr)) return 'selector'
|
|
816
|
+
if (/^\{/.test(returnExpr)) return 'struct'
|
|
817
|
+
if (/^\[/.test(returnExpr)) return 'array'
|
|
818
|
+
|
|
819
|
+
// Check for known builtin return types
|
|
820
|
+
const callMatch = returnExpr.match(/^(\w+)\s*\(/)
|
|
821
|
+
if (callMatch) {
|
|
822
|
+
const fnName = callMatch[1]
|
|
823
|
+
// Common builtins that return int
|
|
824
|
+
if (['scoreboard_get', 'score', 'random', 'random_native', 'str_len', 'len', 'data_get', 'bossbar_get_value', 'set_contains'].includes(fnName)) {
|
|
825
|
+
return 'int'
|
|
826
|
+
}
|
|
827
|
+
if (fnName === 'set_new') return 'string'
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return null
|
|
831
|
+
}
|
|
832
|
+
|
|
780
833
|
function escapeRe(s: string): string {
|
|
781
834
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
782
835
|
}
|
|
@@ -77,6 +77,38 @@ function isStructLiteralField(doc: vscode.TextDocument, position: vscode.Positio
|
|
|
77
77
|
return null
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Check if cursor is on a member access field: expr.field
|
|
82
|
+
* Returns the struct type name if found, null otherwise
|
|
83
|
+
*/
|
|
84
|
+
function isMemberAccessField(doc: vscode.TextDocument, position: vscode.Position, word: string): string | null {
|
|
85
|
+
const line = doc.lineAt(position.line).text
|
|
86
|
+
const wordStart = position.character
|
|
87
|
+
|
|
88
|
+
// Check if word is preceded by '.'
|
|
89
|
+
const beforeWord = line.slice(0, wordStart)
|
|
90
|
+
if (!beforeWord.endsWith('.')) return null
|
|
91
|
+
|
|
92
|
+
// Find the variable before the dot
|
|
93
|
+
const varMatch = beforeWord.match(/(\w+)\s*\.$/)
|
|
94
|
+
if (!varMatch) return null
|
|
95
|
+
const varName = varMatch[1]
|
|
96
|
+
|
|
97
|
+
// Find the variable's type declaration
|
|
98
|
+
const text = doc.getText()
|
|
99
|
+
// Look for: let varName: TypeName or fn param varName: TypeName
|
|
100
|
+
const typeRe = new RegExp(`\\b(?:let|const)\\s+${varName}\\s*:\\s*(\\w+)`, 'm')
|
|
101
|
+
const typeMatch = text.match(typeRe)
|
|
102
|
+
if (typeMatch) return typeMatch[1]
|
|
103
|
+
|
|
104
|
+
// Also check function parameters: fn xxx(varName: TypeName)
|
|
105
|
+
const paramRe = new RegExp(`\\((?:[^)]*,\\s*)?${varName}\\s*:\\s*(\\w+)`, 'm')
|
|
106
|
+
const paramMatch = text.match(paramRe)
|
|
107
|
+
if (paramMatch) return paramMatch[1]
|
|
108
|
+
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
function findAllOccurrences(doc: vscode.TextDocument, word: string): vscode.Location[] {
|
|
81
113
|
const text = doc.getText()
|
|
82
114
|
const re = new RegExp(`\\b${escapeRegex(word)}\\b`, 'g')
|
|
@@ -119,6 +151,16 @@ export function registerSymbolProviders(context: vscode.ExtensionContext): void
|
|
|
119
151
|
}
|
|
120
152
|
}
|
|
121
153
|
|
|
154
|
+
// Check if this is a member access: expr.field
|
|
155
|
+
const memberAccess = isMemberAccessField(doc, position, word)
|
|
156
|
+
if (memberAccess) {
|
|
157
|
+
const structFields = findStructFields(doc)
|
|
158
|
+
const field = structFields.find(f => f.structName === memberAccess && f.fieldName === word)
|
|
159
|
+
if (field) {
|
|
160
|
+
return new vscode.Location(doc.uri, field.fieldRange)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
const decls = findDeclarations(doc)
|
|
123
165
|
const decl = decls.find(d => d.name === word)
|
|
124
166
|
if (!decl) return null
|
package/package.json
CHANGED
|
@@ -10,11 +10,11 @@ describe('CLI API', () => {
|
|
|
10
10
|
describe('imports', () => {
|
|
11
11
|
it('compiles a file with imported helpers', () => {
|
|
12
12
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-imports-'))
|
|
13
|
-
const libPath = path.join(tempDir, 'lib.
|
|
14
|
-
const mainPath = path.join(tempDir, 'main.
|
|
13
|
+
const libPath = path.join(tempDir, 'lib.mcrs')
|
|
14
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
15
15
|
|
|
16
16
|
fs.writeFileSync(libPath, 'fn double(x: int) -> int { return x + x; }\n')
|
|
17
|
-
fs.writeFileSync(mainPath, 'import "./lib.
|
|
17
|
+
fs.writeFileSync(mainPath, 'import "./lib.mcrs"\n\nfn main() { let value: int = double(2); }\n')
|
|
18
18
|
|
|
19
19
|
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
20
20
|
const result = compile(source, { namespace: 'imports', filePath: mainPath })
|
|
@@ -26,13 +26,13 @@ describe('CLI API', () => {
|
|
|
26
26
|
|
|
27
27
|
it('deduplicates circular imports', () => {
|
|
28
28
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-circular-'))
|
|
29
|
-
const aPath = path.join(tempDir, 'a.
|
|
30
|
-
const bPath = path.join(tempDir, 'b.
|
|
31
|
-
const mainPath = path.join(tempDir, 'main.
|
|
29
|
+
const aPath = path.join(tempDir, 'a.mcrs')
|
|
30
|
+
const bPath = path.join(tempDir, 'b.mcrs')
|
|
31
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
32
32
|
|
|
33
|
-
fs.writeFileSync(aPath, 'import "./b.
|
|
34
|
-
fs.writeFileSync(bPath, 'import "./a.
|
|
35
|
-
fs.writeFileSync(mainPath, 'import "./a.
|
|
33
|
+
fs.writeFileSync(aPath, 'import "./b.mcrs"\n\nfn from_a() -> int { return 1; }\n')
|
|
34
|
+
fs.writeFileSync(bPath, 'import "./a.mcrs"\n\nfn from_b() -> int { return from_a(); }\n')
|
|
35
|
+
fs.writeFileSync(mainPath, 'import "./a.mcrs"\n\nfn main() { let value: int = from_b(); }\n')
|
|
36
36
|
|
|
37
37
|
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
38
38
|
const result = compile(source, { namespace: 'circular', filePath: mainPath })
|
|
@@ -40,6 +40,172 @@ describe('CLI API', () => {
|
|
|
40
40
|
expect(result.ir.functions.filter(fn => fn.name === 'from_a')).toHaveLength(1)
|
|
41
41
|
expect(result.ir.functions.filter(fn => fn.name === 'from_b')).toHaveLength(1)
|
|
42
42
|
})
|
|
43
|
+
|
|
44
|
+
it('uses rs-prefixed scoreboard objectives for imported stdlib files', () => {
|
|
45
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-'))
|
|
46
|
+
const stdlibDir = path.join(tempDir, 'src', 'stdlib')
|
|
47
|
+
const stdlibPath = path.join(stdlibDir, 'timer.mcrs')
|
|
48
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
49
|
+
|
|
50
|
+
fs.mkdirSync(stdlibDir, { recursive: true })
|
|
51
|
+
fs.writeFileSync(stdlibPath, 'fn tick_timer() { scoreboard_set("#rs", "timer_ticks", 1); }\n')
|
|
52
|
+
fs.writeFileSync(mainPath, 'import "./src/stdlib/timer.mcrs"\n\nfn main() { tick_timer(); }\n')
|
|
53
|
+
|
|
54
|
+
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
55
|
+
const result = compile(source, { namespace: 'mygame', filePath: mainPath })
|
|
56
|
+
const tickTimer = result.files.find(file => file.path.endsWith('/tick_timer.mcfunction'))
|
|
57
|
+
|
|
58
|
+
expect(tickTimer?.content).toContain('scoreboard players set #rs rs.timer_ticks 1')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('adds a call-site hash for stdlib internal scoreboard objectives', () => {
|
|
62
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-hash-'))
|
|
63
|
+
const stdlibDir = path.join(tempDir, 'src', 'stdlib')
|
|
64
|
+
const stdlibPath = path.join(stdlibDir, 'timer.mcrs')
|
|
65
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
66
|
+
|
|
67
|
+
fs.mkdirSync(stdlibDir, { recursive: true })
|
|
68
|
+
fs.writeFileSync(stdlibPath, [
|
|
69
|
+
'fn timer_start(name: string, duration: int) {',
|
|
70
|
+
' scoreboard_set("timer_ticks", #rs, duration);',
|
|
71
|
+
' scoreboard_set("timer_active", #rs, 1);',
|
|
72
|
+
'}',
|
|
73
|
+
'',
|
|
74
|
+
].join('\n'))
|
|
75
|
+
fs.writeFileSync(mainPath, [
|
|
76
|
+
'import "./src/stdlib/timer.mcrs"',
|
|
77
|
+
'',
|
|
78
|
+
'fn main() {',
|
|
79
|
+
' timer_start("x", 100);',
|
|
80
|
+
' timer_start("x", 100);',
|
|
81
|
+
'}',
|
|
82
|
+
'',
|
|
83
|
+
].join('\n'))
|
|
84
|
+
|
|
85
|
+
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
86
|
+
const result = compile(source, { namespace: 'mygame', filePath: mainPath })
|
|
87
|
+
const timerFns = result.files.filter(file => /timer_start__callsite_[0-9a-f]{4}\.mcfunction$/.test(file.path))
|
|
88
|
+
|
|
89
|
+
expect(timerFns).toHaveLength(2)
|
|
90
|
+
|
|
91
|
+
const objectives = timerFns
|
|
92
|
+
.flatMap(file => [...file.content.matchAll(/rs\._timer_([0-9a-f]{4})/g)].map(match => match[0]))
|
|
93
|
+
|
|
94
|
+
expect(new Set(objectives).size).toBe(2)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('Timer::new creates timer', () => {
|
|
98
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-new-'))
|
|
99
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
100
|
+
const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
|
|
101
|
+
|
|
102
|
+
fs.writeFileSync(mainPath, [
|
|
103
|
+
`import "${timerPath}"`,
|
|
104
|
+
'',
|
|
105
|
+
'fn main() {',
|
|
106
|
+
' let timer: Timer = Timer::new(20);',
|
|
107
|
+
'}',
|
|
108
|
+
'',
|
|
109
|
+
].join('\n'))
|
|
110
|
+
|
|
111
|
+
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
112
|
+
const result = compile(source, { namespace: 'timernew', filePath: mainPath })
|
|
113
|
+
|
|
114
|
+
expect(result.typeErrors).toEqual([])
|
|
115
|
+
const newFn = result.files.find(file => file.path.endsWith('/Timer_new.mcfunction'))
|
|
116
|
+
expect(newFn?.content).toContain('scoreboard players set timer_ticks rs 0')
|
|
117
|
+
expect(newFn?.content).toContain('scoreboard players set timer_active rs 0')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('Timer.start/pause/reset', () => {
|
|
121
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-state-'))
|
|
122
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
123
|
+
const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(mainPath, [
|
|
126
|
+
`import "${timerPath}"`,
|
|
127
|
+
'',
|
|
128
|
+
'fn main() {',
|
|
129
|
+
' let timer: Timer = Timer::new(20);',
|
|
130
|
+
' timer.start();',
|
|
131
|
+
' timer.pause();',
|
|
132
|
+
' timer.reset();',
|
|
133
|
+
'}',
|
|
134
|
+
'',
|
|
135
|
+
].join('\n'))
|
|
136
|
+
|
|
137
|
+
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
138
|
+
const result = compile(source, { namespace: 'timerstate', filePath: mainPath })
|
|
139
|
+
|
|
140
|
+
expect(result.typeErrors).toEqual([])
|
|
141
|
+
const startFn = result.files.find(file => file.path.endsWith('/Timer_start.mcfunction'))
|
|
142
|
+
const pauseFn = result.files.find(file => file.path.endsWith('/Timer_pause.mcfunction'))
|
|
143
|
+
const resetFn = result.files.find(file => file.path.endsWith('/Timer_reset.mcfunction'))
|
|
144
|
+
|
|
145
|
+
expect(startFn?.content).toContain('scoreboard players set timer_active rs 1')
|
|
146
|
+
expect(pauseFn?.content).toContain('scoreboard players set timer_active rs 0')
|
|
147
|
+
expect(resetFn?.content).toContain('scoreboard players set timer_ticks rs 0')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('Timer.done returns bool', () => {
|
|
151
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-done-'))
|
|
152
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
153
|
+
const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
|
|
154
|
+
|
|
155
|
+
fs.writeFileSync(mainPath, [
|
|
156
|
+
`import "${timerPath}"`,
|
|
157
|
+
'',
|
|
158
|
+
'fn main() {',
|
|
159
|
+
' let timer: Timer = Timer::new(20);',
|
|
160
|
+
' let finished: bool = timer.done();',
|
|
161
|
+
' if (finished) {',
|
|
162
|
+
' say("done");',
|
|
163
|
+
' }',
|
|
164
|
+
'}',
|
|
165
|
+
'',
|
|
166
|
+
].join('\n'))
|
|
167
|
+
|
|
168
|
+
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
169
|
+
const result = compile(source, { namespace: 'timerdone', filePath: mainPath })
|
|
170
|
+
|
|
171
|
+
expect(result.typeErrors).toEqual([])
|
|
172
|
+
const doneFn = result.files.find(file => file.path.endsWith('/Timer_done.mcfunction'))
|
|
173
|
+
const mainFn = result.files.find(file => file.path.endsWith('/main.mcfunction'))
|
|
174
|
+
expect(doneFn?.content).toContain('scoreboard players get timer_ticks rs')
|
|
175
|
+
expect(doneFn?.content).toContain('return run scoreboard players get')
|
|
176
|
+
expect(mainFn?.content).toContain('execute if score $finished rs matches 1..')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('Timer.tick increments', () => {
|
|
180
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-tick-'))
|
|
181
|
+
const mainPath = path.join(tempDir, 'main.mcrs')
|
|
182
|
+
const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs')
|
|
183
|
+
|
|
184
|
+
fs.writeFileSync(mainPath, [
|
|
185
|
+
`import "${timerPath}"`,
|
|
186
|
+
'',
|
|
187
|
+
'fn main() {',
|
|
188
|
+
' let timer: Timer = Timer::new(20);',
|
|
189
|
+
' timer.start();',
|
|
190
|
+
' timer.tick();',
|
|
191
|
+
'}',
|
|
192
|
+
'',
|
|
193
|
+
].join('\n'))
|
|
194
|
+
|
|
195
|
+
const source = fs.readFileSync(mainPath, 'utf-8')
|
|
196
|
+
const result = compile(source, { namespace: 'timertick', filePath: mainPath })
|
|
197
|
+
|
|
198
|
+
expect(result.typeErrors).toEqual([])
|
|
199
|
+
const tickOutput = result.files
|
|
200
|
+
.filter(file => file.path.includes('/Timer_tick'))
|
|
201
|
+
.map(file => file.content)
|
|
202
|
+
.join('\n')
|
|
203
|
+
|
|
204
|
+
expect(tickOutput).toContain('scoreboard players get timer_active rs')
|
|
205
|
+
expect(tickOutput).toContain('scoreboard players get timer_ticks rs')
|
|
206
|
+
expect(tickOutput).toContain(' += $const_1 rs')
|
|
207
|
+
expect(tickOutput).toContain('execute store result score timer_ticks rs run scoreboard players get $_')
|
|
208
|
+
})
|
|
43
209
|
})
|
|
44
210
|
|
|
45
211
|
describe('compile()', () => {
|
|
@@ -91,7 +257,7 @@ fn build() {
|
|
|
91
257
|
describe('--stats flag', () => {
|
|
92
258
|
it('prints optimizer statistics', () => {
|
|
93
259
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stats-'))
|
|
94
|
-
const inputPath = path.join(tempDir, 'input.
|
|
260
|
+
const inputPath = path.join(tempDir, 'input.mcrs')
|
|
95
261
|
const outputDir = path.join(tempDir, 'out')
|
|
96
262
|
|
|
97
263
|
fs.writeFileSync(inputPath, 'fn build() { setblock((0, 64, 0), "minecraft:stone"); setblock((1, 64, 0), "minecraft:stone"); }')
|
|
@@ -11,7 +11,7 @@ describe('generateDatapack', () => {
|
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
it('generates __load.mcfunction with objective setup', () => {
|
|
14
|
-
const mod: IRModule = { namespace: 'mypack', functions: [], globals: ['counter'] }
|
|
14
|
+
const mod: IRModule = { namespace: 'mypack', functions: [], globals: [{ name: 'counter', init: 0 }] }
|
|
15
15
|
const files = generateDatapack(mod)
|
|
16
16
|
const load = files.find(f => f.path.includes('__load.mcfunction'))
|
|
17
17
|
expect(load?.content).toContain('scoreboard objectives add rs dummy')
|
|
@@ -125,4 +125,31 @@ describe('generateDatapack', () => {
|
|
|
125
125
|
expect(json.criteria.trigger.trigger).toBe('minecraft:story/mine_diamond')
|
|
126
126
|
expect(json.rewards.function).toBe('mypack:on_mine_diamond')
|
|
127
127
|
})
|
|
128
|
+
|
|
129
|
+
it('generates static event dispatcher in __tick', () => {
|
|
130
|
+
const mod: IRModule = {
|
|
131
|
+
namespace: 'mypack',
|
|
132
|
+
globals: [],
|
|
133
|
+
functions: [{
|
|
134
|
+
name: 'handle_death',
|
|
135
|
+
params: [],
|
|
136
|
+
locals: [],
|
|
137
|
+
blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
|
|
138
|
+
eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
|
|
139
|
+
}, {
|
|
140
|
+
name: 'handle_death_2',
|
|
141
|
+
params: [],
|
|
142
|
+
locals: [],
|
|
143
|
+
blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
|
|
144
|
+
eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
|
|
145
|
+
}],
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const files = generateDatapack(mod)
|
|
149
|
+
const tickFn = files.find(f => f.path.includes('__tick.mcfunction'))
|
|
150
|
+
expect(tickFn).toBeDefined()
|
|
151
|
+
expect(tickFn!.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death')
|
|
152
|
+
expect(tickFn!.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death_2')
|
|
153
|
+
expect(tickFn!.content).toContain('tag @a[tag=rs.just_died] remove rs.just_died')
|
|
154
|
+
})
|
|
128
155
|
})
|
|
@@ -33,11 +33,11 @@ describe('DiagnosticError', () => {
|
|
|
33
33
|
const error = new DiagnosticError(
|
|
34
34
|
'TypeError',
|
|
35
35
|
'Unknown function: foo',
|
|
36
|
-
{ file: 'test.
|
|
36
|
+
{ file: 'test.mcrs', line: 1, col: 9 },
|
|
37
37
|
source.split('\n')
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
expect(formatError(error, source)).toContain('Error in test.
|
|
40
|
+
expect(formatError(error, source)).toContain('Error in test.mcrs at line 1, col 9:')
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
43
|
|
|
@@ -66,11 +66,11 @@ describe('DiagnosticError', () => {
|
|
|
66
66
|
const error = new DiagnosticError(
|
|
67
67
|
'LexError',
|
|
68
68
|
'Unexpected character',
|
|
69
|
-
{ file: 'test.
|
|
69
|
+
{ file: 'test.mcrs', line: 1, col: 1 },
|
|
70
70
|
['@@@']
|
|
71
71
|
)
|
|
72
72
|
const formatted = error.format()
|
|
73
|
-
expect(formatted).toContain('test.
|
|
73
|
+
expect(formatted).toContain('test.mcrs:')
|
|
74
74
|
expect(formatted).toContain('[LexError]')
|
|
75
75
|
})
|
|
76
76
|
|
|
@@ -153,7 +153,7 @@ describe('compile function', () => {
|
|
|
153
153
|
})
|
|
154
154
|
|
|
155
155
|
it('includes file path in error', () => {
|
|
156
|
-
const result = compile('fn main() { }', { filePath: 'test.
|
|
156
|
+
const result = compile('fn main() { }', { filePath: 'test.mcrs' })
|
|
157
157
|
// This is valid, but test that filePath is passed through
|
|
158
158
|
expect(result.success).toBe(true)
|
|
159
159
|
})
|