├── .gitignore ├── .prettierignore ├── .travis.yml ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CODEOWNERS ├── LICENSE ├── README.md ├── bors.toml ├── dist └── index.js ├── lintrules └── helperReturnRule.ts ├── mocha.opts ├── package.json ├── renovate.json ├── scripts ├── .gitignore └── nbt_path.js ├── src ├── actions.ts ├── brigadier │ ├── errors.ts │ └── string-reader.ts ├── colors.ts ├── completions.ts ├── consts.ts ├── data │ ├── cache.ts │ ├── datapack-resources.ts │ ├── extractor │ │ ├── collect-data.ts │ │ ├── download.ts │ │ ├── extract-data.ts │ │ ├── index.ts │ │ └── mapfunctions.ts │ ├── lists │ │ ├── criteria.ts │ │ ├── item-slot.ts │ │ ├── scoreboard-slot.ts │ │ └── statics.ts │ ├── manager.ts │ ├── nbt │ │ ├── buffer-stream.ts │ │ ├── nbt-cache.ts │ │ ├── nbt-types.ts │ │ └── parser.ts │ ├── noncached.ts │ └── types.ts ├── declaration.d.ts ├── index.ts ├── misc-functions │ ├── context.ts │ ├── creators.ts │ ├── datapack-folder.ts │ ├── file-errors.ts │ ├── group-resources.ts │ ├── index.ts │ ├── lsp-conversions.ts │ ├── namespace.ts │ ├── node-tree.ts │ ├── parsing │ │ ├── namespace.ts │ │ └── nmsp-tag.ts │ ├── promisified-fs.ts │ ├── return-helper.ts │ ├── security.ts │ ├── setup.ts │ ├── third_party │ │ ├── child-dir.ts │ │ ├── merge-deep.ts │ │ └── typed-keys.ts │ └── translation.ts ├── parse.ts ├── parsers │ ├── brigadier │ │ ├── bool.ts │ │ ├── double.ts │ │ ├── float.ts │ │ ├── index.ts │ │ ├── integer.ts │ │ └── string.ts │ ├── get-parser.ts │ ├── literal.ts │ └── minecraft │ │ ├── block.ts │ │ ├── component.ts │ │ ├── coordinates.ts │ │ ├── entity.ts │ │ ├── item.ts │ │ ├── lists.ts │ │ ├── message.ts │ │ ├── namespace-list.ts │ │ ├── nbt-path.ts │ │ ├── nbt │ │ ├── doc-walker-func.ts │ │ ├── nbt.ts │ │ ├── tag-parser.ts │ │ ├── tag │ │ │ ├── compound-tag.ts │ │ │ ├── list-tag.ts │ │ │ ├── lists.ts │ │ │ ├── nbt-tag.ts │ │ │ ├── number.ts │ │ │ ├── string-tag.ts │ │ │ └── typed-list-tag.ts │ │ ├── util │ │ │ ├── array-reader.ts │ │ │ ├── doc-walker-util.ts │ │ │ └── nbt-util.ts │ │ └── walker.ts │ │ ├── range.ts │ │ ├── resources.ts │ │ ├── scoreboard.ts │ │ ├── swizzle.ts │ │ └── time.ts ├── test │ ├── actions.test.ts │ ├── assertions.ts │ ├── blanks.ts │ ├── brigadier │ │ └── string-reader.test.ts │ ├── completions.test.ts │ ├── data │ │ ├── NOTICE.txt │ │ ├── cache.test.ts │ │ ├── datapack-resource.test.ts │ │ └── nbt.test.ts │ ├── logging-setup.ts │ ├── misc-functions │ │ ├── context.test.ts │ │ ├── creators.test.ts │ │ ├── datapack-folder.test.ts │ │ ├── group-resources.test.ts │ │ ├── lsp-conversions.test.ts │ │ ├── namespace.test.ts │ │ ├── node-tree.test.ts │ │ └── parsing │ │ │ ├── namespace.test.ts │ │ │ └── nmsp-tag.test.ts │ ├── parse.test.ts │ ├── parsers │ │ ├── brigadier │ │ │ ├── bool.test.ts │ │ │ ├── double.test.ts │ │ │ ├── float.test.ts │ │ │ ├── integer.test.ts │ │ │ └── string.test.ts │ │ ├── get-parser.test.ts │ │ ├── literal.test.ts │ │ ├── minecraft │ │ │ ├── block.test.ts │ │ │ ├── coord.test.ts │ │ │ ├── entity.test.ts │ │ │ ├── item.test.ts │ │ │ ├── lists.test.ts │ │ │ ├── message.test.ts │ │ │ ├── namespace-list.test.ts │ │ │ ├── nbt │ │ │ │ ├── doc-walker.test.ts │ │ │ │ ├── nbt.test.ts │ │ │ │ ├── tag-parser-data.ts │ │ │ │ ├── tag-parser.test.ts │ │ │ │ └── test-data.ts │ │ │ ├── range.test.ts │ │ │ ├── resources.test.ts │ │ │ ├── scoreboard.test.ts │ │ │ ├── swizzle.test.ts │ │ │ └── time.test.ts │ │ └── tests │ │ │ ├── dummy1.test.ts │ │ │ └── dummy1.ts │ └── untested.md ├── types.ts └── util.ts ├── test_data ├── test_nbt │ └── bigtest.nbt └── test_world │ ├── datapacks │ ├── ExampleDatapack │ │ ├── data │ │ │ ├── minecraft │ │ │ │ └── tags │ │ │ │ │ └── functions │ │ │ │ │ └── tick.json │ │ │ └── test_namespace │ │ │ │ └── functions │ │ │ │ └── function.mcfunction │ │ └── pack.mcmeta │ └── ExampleDatapack2 │ │ ├── data │ │ └── test_namespace │ │ │ └── functions │ │ │ └── function.mcfunction │ │ └── pack.mcmeta │ └── level.dat ├── todo.md ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .cache 4 | dist/index.map 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | branches: 3 | only: 4 | # This is where pull requests from "bors r+" are built. 5 | - staging 6 | # This is where pull requests from "bors try" are built. 7 | - trying 8 | - master 9 | sudo: false 10 | node_js: 11 | # - stable 12 | # For vscode - same as stable 13 | - "10" 14 | before_install: 15 | - npm install -g npm@6 16 | install: 17 | - npm install 18 | script: 19 | - npm run check 20 | - npm test 21 | - npm run lint 22 | - npm run prettier -- --list-different 23 | - npm run build 24 | - git diff --exit-code dist/index.js 25 | # This should be working as of parcel#2020, but it isn't 26 | # - git diff dist/index.map --exit-code 27 | env: 28 | global: 29 | secure: MRJdpa8qRLcMNDKhFi1OP9lalqcqVt9EK85SBuPdL63sA3RNIdu5EJw/aGCf6pJ7GJYXMTiZh/sLbUh/1mybHvP0WYu10uJxCacejhRc+dB2Txz/zqWYWbjMXLas7MX+MY8A+ChgjClJKWGZc9Xl6W67UAC6MW0aS0L+9SGRWXZwfVwPBsZDGF23694tcsaIpqvFENTD0dO9o0TX9dPsdulzA6tIWOtXrYDqdmneSQBr6VYsjobMslB9ScOgVNxU9pyPNhgvzMp5juboJLsnBohWDk1aocaQT6Qfh+24Zvic0Mw8nbh0kgiWC4sNaKwhpceh6uKiG2doKVYwABCgGdcBsc5/kkELzJYvoHJsGSABCiBZA1t7gFSz2Q1DEcyeGzsEAapESJcpYsO2hOoaZpdwR4QYd31Z+cyyIoun71nv+dl0x15Iq6Wj4VtAm8eZQlZJaXTK2Fbw0jC1MAkvpf+llaNB55xnEa0mNCvi/NVVxS03diONvBC4PiA9pp9hrqaetyfG0CR+ugCcapPj46hXf2TJo3FjWn1QbqElcHmc8y5+bNrstHUtyNazby5yXZQO7+41vHaSUaCz2mOOe0TlTAQOiCeyfNJhP6tTLzsQ5n7y0eA+KczWXScR1wA4Lm2lZEQ8Vgw15j1kDkBUoXBOUnZtXFSHaUn51yWxQpc= 30 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": ["--opts", "${workspaceFolder}/mocha.opts"], 13 | "internalConsoleOptions": "openOnSessionStart", 14 | "cwd": "${workspaceFolder}" 15 | // "stopOnEntry": true 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | //=======================================// 3 | // Code Style // 4 | //=======================================// 5 | // Automatic Code Cleaning 6 | "editor.formatOnSave": true, 7 | // Allow use of prettier extension solely 8 | "typescript.format.enable": false, 9 | // Consistent finalNewLine 10 | "files.insertFinalNewline": true, 11 | // Use of node_modules folder typescript version 12 | "typescript.tsdk": "node_modules\\typescript\\lib", 13 | "search.exclude": { 14 | "**/node_modules": true, 15 | "**/bower_components": true, 16 | "**/dist": true 17 | }, 18 | "editor.codeActionsOnSave": { 19 | "source.fixAll.tslint": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "check", 9 | "problemMatcher": ["$tsc"], 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | }, 15 | { 16 | "type": "npm", 17 | "script": "lint", 18 | "problemMatcher": ["$tslint5"] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Levertion 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Levertion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Minecraft Function Language Server 2 | 3 | ## This project is now deprecated - you should prefer to use the excellent [SPYGlass](https://github.com/SPYGlassMC/SPYGlass) project 4 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | "continuous-integration/travis-ci/push", 3 | ] 4 | -------------------------------------------------------------------------------- /lintrules/helperReturnRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from "tslint"; 2 | import * as ts from "typescript"; 3 | 4 | export class Rule extends Lint.Rules.TypedRule { 5 | public static FAILURE_STRING = 6 | "return in function returning info without using helper"; 7 | 8 | public applyWithProgram( 9 | sourceFile: ts.SourceFile, 10 | program: ts.Program 11 | ): Lint.RuleFailure[] { 12 | return this.applyWithWalker( 13 | new HelperReturnWalker(sourceFile, this.getOptions(), program) 14 | ); 15 | } 16 | } 17 | 18 | const keys = ["actions", "errors", "misc", "suggestions"]; 19 | // The walker takes care of all the work. 20 | // tslint:disable-next-line:max-classes-per-file 21 | class HelperReturnWalker extends Lint.RuleWalker { 22 | private program: ts.Program; 23 | public constructor( 24 | source: ts.SourceFile, 25 | options: Lint.IOptions, 26 | program: ts.Program 27 | ) { 28 | super(source, options); 29 | this.program = program; 30 | } 31 | 32 | public visitReturnStatement(node: ts.ReturnStatement): void { 33 | const passes = 34 | node.expression && node.expression.getText().startsWith("helper"); 35 | if (!passes) { 36 | if (node.expression) { 37 | const type = this.program 38 | .getTypeChecker() 39 | .getContextualType(node.expression); 40 | if (type) { 41 | const props = type.getApparentProperties().map(v => v.name); 42 | const matches = keys.every( 43 | prop => props.indexOf(prop) !== -1 44 | ); 45 | if (matches) { 46 | const fix = node.expression 47 | ? new Lint.Replacement( 48 | node.expression.getStart(), 49 | node.expression.getWidth(), 50 | `helper.return(${node.expression.getText()})` 51 | ) 52 | : undefined; 53 | this.addFailureAtNode(node, Rule.FAILURE_STRING, fix); 54 | } 55 | } 56 | } 57 | } 58 | super.visitReturnStatement(node); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --ui bdd 2 | --colors 3 | --timeout 999999 4 | --require ts-mocha 5 | --require ./src/test/logging-setup.ts 6 | src/test/**/*.test.ts 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcfunction-langserver", 3 | "description": "Language Server for Minecraft Functions", 4 | "version": "0.0.1", 5 | "author": "Levertion", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "scripts": { 9 | "build": "parcel build src/index.ts --target=node --no-minify", 10 | "test": "mocha --opts ./mocha.opts", 11 | "check": "tsc -p tsconfig.json --noEmit", 12 | "lint": "ts-node node_modules/tslint/bin/tslint --project .", 13 | "prettier": "prettier **/*.{ts,json,yml}", 14 | "prettier:write": "npm run prettier -- --write" 15 | }, 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "pretty-quick --staged && npm run build && git add dist" 19 | } 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/Levertion/mcfunction-langserver/" 24 | }, 25 | "files": [ 26 | "dist", 27 | "src" 28 | ], 29 | "keywords": [ 30 | "Minecraft" 31 | ], 32 | "prettier": { 33 | "tabWidth": 4, 34 | "proseWrap": "always" 35 | }, 36 | "dependencies": { 37 | "js-combinatorics": "^0.5.4", 38 | "long": "^4.0.0", 39 | "mc-nbt-paths": "git+https://github.com/MrYurihi/mc-nbt-paths.git", 40 | "minecraft-json-schemas": "git+https://github.com/Levertion/minecraft-json-schemas.git", 41 | "node-interval-tree": "^1.3.3", 42 | "request": "^2.88.0", 43 | "request-promise-native": "^1.0.5", 44 | "sprintf-js": "^1.1.2", 45 | "synchronous-promise": "^2.0.6", 46 | "tslib": "^1.9.3", 47 | "vscode-json-languageservice": "^3.2.0", 48 | "vscode-languageserver": "^5.2.1", 49 | "vscode-uri": "^1.0.6" 50 | }, 51 | "devDependencies": { 52 | "@types/js-combinatorics": "0.5.31", 53 | "@types/long": "4.0.0", 54 | "@types/mocha": "5.2.6", 55 | "@types/node": "10.12.27", 56 | "@types/request-promise-native": "1.0.15", 57 | "@types/rimraf": "2.0.2", 58 | "@types/sprintf-js": "1.1.2", 59 | "husky": "1.3.1", 60 | "mocha": "6.0.2", 61 | "parcel": "1.11.0", 62 | "prettier": "1.16.4", 63 | "pretty-quick": "1.10.0", 64 | "rimraf": "2.6.3", 65 | "ts-mocha": "6.0.0", 66 | "tslint": "5.13.0", 67 | "tslint-config-prettier": "1.18.0", 68 | "typescript": "3.3.3333", 69 | "vscode-languageserver-types": "3.14.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "schedule:weekly"], 3 | "groupName": "all" 4 | } 5 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | commands.json 2 | nbt_path_output.json 3 | -------------------------------------------------------------------------------- /scripts/nbt_path.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | const { readFileSync, writeFileSync } = require("fs"); 3 | const { join } = require("path"); 4 | /** 5 | * @param {import("../src/data/types").CommandNode} node 6 | * @param {string[]} path 7 | * @returns {string[][]} 8 | */ 9 | function getPaths(node, path) { 10 | const result = []; 11 | if (node.parser === "minecraft:nbt_path") { 12 | result.push(path); 13 | } 14 | if (node.children) { 15 | for (const child of Object.keys(node.children)) { 16 | result.push(...getPaths(node.children[child], [...path, child])); 17 | } 18 | } 19 | return result; 20 | } 21 | 22 | writeFileSync( 23 | join(__dirname, "nbt_path_output.json"), 24 | JSON.stringify( 25 | simplify( 26 | getPaths( 27 | JSON.parse( 28 | readFileSync(join(__dirname, "commands.json")).toString() 29 | ), 30 | [] 31 | ) 32 | ), 33 | undefined, 34 | 4 35 | ) 36 | ); 37 | 38 | /** 39 | * Get the minimum paths which start with a given value 40 | * @param {string[][]} param 41 | * @returns {string[][]} 42 | */ 43 | function simplify(param) { 44 | const result = []; 45 | outer: for (const option of param) { 46 | const internal = []; 47 | for (const part of option) { 48 | internal.push(part); 49 | if ( 50 | param.filter(v => internal.every((a, i) => a === v[i])) 51 | .length === 1 52 | ) { 53 | result.push(internal); 54 | continue outer; 55 | } 56 | } 57 | result.push(internal); 58 | } 59 | return result; 60 | } 61 | -------------------------------------------------------------------------------- /src/brigadier/errors.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver/lib/main"; 2 | 3 | import { MCFormat } from "../misc-functions"; 4 | import { LineRange } from "../types"; 5 | 6 | /** 7 | * A blank command error 8 | */ 9 | export interface BlankCommandError { 10 | /** 11 | * The code of this error, usable for translation? 12 | */ 13 | code: string; 14 | /** 15 | * The severity of this error. 16 | */ 17 | severity: DiagnosticSeverity; 18 | /** 19 | * The substitutions to insert into the error text. 20 | */ 21 | substitutions?: string[]; 22 | /** 23 | * The cached text of this error. 24 | */ 25 | text: string; 26 | } 27 | 28 | /** 29 | * An error inside a command. 30 | */ 31 | export interface CommandError extends BlankCommandError { 32 | /** 33 | * The range of this error. 34 | */ 35 | range: LineRange; 36 | } 37 | 38 | /** 39 | * Helper class to create command errors 40 | */ 41 | export class CommandErrorBuilder { 42 | private readonly code: string; 43 | private readonly default: string; 44 | private readonly severity: DiagnosticSeverity; 45 | 46 | public constructor( 47 | code: string, 48 | explanation: string, 49 | severity: DiagnosticSeverity = DiagnosticSeverity.Error 50 | ) { 51 | this.code = code; 52 | this.default = explanation; 53 | this.severity = severity; 54 | } 55 | 56 | public create( 57 | start: number, 58 | end: number, 59 | ...substitutions: string[] 60 | ): CommandError { 61 | const diagnosis: CommandError = Object.assign( 62 | this.createBlank(...substitutions), 63 | { range: { start, end } } 64 | ); 65 | return diagnosis; 66 | } 67 | 68 | public createBlank(...substitutions: string[]): BlankCommandError { 69 | return { 70 | code: this.code, 71 | severity: this.severity, 72 | substitutions, 73 | text: MCFormat(this.default, ...substitutions) 74 | }; 75 | } 76 | } 77 | 78 | /** 79 | * Transform `err` into a real command error. 80 | * MODIFIES `err` 81 | * @param err The error to transform 82 | * @param start The starting location in the line of the error 83 | * @param end The end position 84 | */ 85 | export function fillBlankError( 86 | err: BlankCommandError, 87 | start: number, 88 | end: number 89 | ): CommandError { 90 | return { ...err, range: { start, end } }; 91 | } 92 | -------------------------------------------------------------------------------- /src/colors.ts: -------------------------------------------------------------------------------- 1 | export const COLORS = [ 2 | "black", 3 | "dark_blue", 4 | "dark_green", 5 | "dark_aqua", 6 | "dark_red", 7 | "dark_purple", 8 | "gold", 9 | "gray", 10 | "dark_gray", 11 | "blue", 12 | "green", 13 | "aqua", 14 | "red", 15 | "light_purple", 16 | "yellow", 17 | "white", 18 | "reset" 19 | ]; 20 | -------------------------------------------------------------------------------- /src/consts.ts: -------------------------------------------------------------------------------- 1 | export const COMMENT_START = "#"; 2 | export const SPACE = " "; 3 | 4 | // Namespaces 5 | export const DEFAULT_NAMESPACE = "minecraft"; 6 | export const NAMESPACE = ":"; 7 | export const DATAFOLDER = "data"; 8 | export const SLASH = "/"; 9 | export const SLASHREGEX = /\//g; 10 | export const SLASHREPLACEREGEX = /\\/g; 11 | export const MCMETAFILE = "pack.mcmeta"; 12 | 13 | // Blocks 14 | export const TAG_START = "#"; 15 | 16 | // Misc 17 | 18 | export const JAVAMAXINT = 2147483647; 19 | export const JAVAMININT = -2147483648; 20 | 21 | export const NONWHITESPACE = /\S/; 22 | -------------------------------------------------------------------------------- /src/data/cache.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { 4 | mkdirAsync, 5 | readJSONRaw, 6 | saveFileAsync, 7 | writeJSON 8 | } from "../misc-functions/promisified-fs"; 9 | import { typed_keys } from "../misc-functions/third_party/typed-keys"; 10 | import { WorkspaceSecurity } from "../types"; 11 | 12 | import { Cacheable, RegistriesData } from "./types"; 13 | 14 | if (!process.env.MCFUNCTION_CACHE_DIR) { 15 | throw new Error("Environment variable MCFUNCTION_CACHE_DIR must be set"); 16 | } 17 | export const cacheFolder = process.env.MCFUNCTION_CACHE_DIR; 18 | 19 | const cacheFileNames: Record, string> = { 20 | blocks: "blocks.json", 21 | commands: "commands.json", 22 | meta_info: "meta_info.json", 23 | resources: "resources.json" 24 | }; 25 | 26 | const registriesCacheFile = "registries.json"; 27 | 28 | export async function readCache(): Promise { 29 | const data: Cacheable = {} as Cacheable; 30 | const keys = typed_keys(cacheFileNames); 31 | await Promise.all([ 32 | ...keys.map(async key => { 33 | const cacheDir = cacheFileNames[key]; 34 | data[key] = await readJSONRaw( 35 | path.join(cacheFolder, cacheDir) 36 | ); 37 | }), 38 | readRegistries(data) 39 | ]); 40 | return data; 41 | } 42 | 43 | type CachedRegistries = Record; 44 | 45 | async function readRegistries(data: Cacheable): Promise { 46 | const read = await readJSONRaw( 47 | path.join(cacheFolder, registriesCacheFile) 48 | ); 49 | data.registries = {} as any; 50 | for (const key of typed_keys(read)) { 51 | data.registries[key] = new Set(read[key]); 52 | } 53 | } 54 | 55 | export async function cacheData(data: Cacheable): Promise { 56 | try { 57 | await mkdirAsync(cacheFolder, "777"); 58 | } catch (_) { 59 | // Don't use the error, which is normally thrown if the folder already exists 60 | } 61 | const keys: Array = typed_keys(cacheFileNames); 62 | await Promise.all([ 63 | ...keys.map(async key => 64 | writeJSON(path.join(cacheFolder, cacheFileNames[key]), data[key]) 65 | ), 66 | cacheRegistry(data.registries) 67 | ]); 68 | } 69 | 70 | async function cacheRegistry(registries: RegistriesData): Promise { 71 | const toWrite = {} as CachedRegistries; 72 | for (const key of typed_keys(registries)) { 73 | toWrite[key] = [...registries[key]]; 74 | } 75 | await writeJSON(path.join(cacheFolder, registriesCacheFile), toWrite); 76 | } 77 | 78 | export async function storeSecurity( 79 | security: WorkspaceSecurity 80 | ): Promise { 81 | await saveFileAsync( 82 | path.join(cacheFolder, "security.json"), 83 | JSON.stringify(security) 84 | ); 85 | } 86 | 87 | export async function readSecurity(): Promise { 88 | try { 89 | return await readJSONRaw(path.join(cacheFolder, "security.json")); 90 | } catch (error) { 91 | return {}; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/data/extractor/collect-data.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { promisify } from "util"; 4 | 5 | import { DATAFOLDER } from "../../consts"; 6 | import { ReturnHelper } from "../../misc-functions"; 7 | import { typed_keys } from "../../misc-functions/third_party/typed-keys"; 8 | import { ReturnSuccess } from "../../types"; 9 | import { getNamespaceResources } from "../datapack-resources"; 10 | import { 11 | BlocksPropertyInfo, 12 | CommandTree, 13 | GlobalData, 14 | RegistriesData, 15 | RegistryNames 16 | } from "../types"; 17 | 18 | import { runMapFunctions } from "./mapfunctions"; 19 | const readFileAsync = promisify(fs.readFile); 20 | 21 | type DataSaveResult = [T, GlobalData[T]]; 22 | 23 | export async function collectData( 24 | version: string, 25 | dataDir: string 26 | ): Promise> { 27 | const helper = new ReturnHelper(); 28 | const result: GlobalData = { meta_info: { version } } as GlobalData; 29 | const cleanups = await Promise.all([ 30 | getBlocks(dataDir), 31 | getRegistries(dataDir), 32 | getCommands(dataDir), 33 | getResources(dataDir) 34 | ]); 35 | for (const dataType of cleanups) { 36 | result[dataType[0]] = dataType[1]; 37 | } 38 | const resources = await runMapFunctions(result.resources, result, dataDir); 39 | return helper 40 | .mergeChain(resources) 41 | .succeed({ ...result, resources: resources.data }); 42 | } 43 | 44 | //#region Resources 45 | 46 | async function getRegistries( 47 | dataDir: string 48 | ): Promise> { 49 | interface ProtocolID { 50 | protocol_id: number; 51 | } 52 | 53 | interface Registries extends Record {} 54 | interface Registry extends ProtocolID { 55 | entries: { 56 | [key: string]: T & ProtocolID; 57 | }; 58 | } 59 | 60 | const registries: Registries = JSON.parse( 61 | (await readFileAsync( 62 | path.join(dataDir, "reports", "registries.json") 63 | )).toString() 64 | ); 65 | const result = {} as RegistriesData; 66 | for (const key of typed_keys(registries)) { 67 | const registry = registries[key]; 68 | if (registry.entries) { 69 | const set = new Set(); 70 | for (const entry of Object.keys(registry.entries)) { 71 | set.add(entry); 72 | } 73 | result[key] = set; 74 | } 75 | } 76 | return ["registries", result]; 77 | } 78 | 79 | async function getResources( 80 | dataDir: string 81 | ): Promise> { 82 | const dataFolder = path.join(dataDir, DATAFOLDER); 83 | const resources = await getNamespaceResources( 84 | "minecraft", 85 | dataFolder, 86 | undefined 87 | ); 88 | return ["resources", resources.data]; 89 | } 90 | //#endregion 91 | 92 | async function getCommands( 93 | dataDir: string 94 | ): Promise> { 95 | const tree: CommandTree = JSON.parse( 96 | (await readFileAsync( 97 | path.join(dataDir, "reports", "commands.json") 98 | )).toString() 99 | ); 100 | return ["commands", tree]; 101 | } 102 | 103 | //#region Blocks 104 | async function getBlocks(dataDir: string): Promise> { 105 | interface BlocksJson { 106 | [id: string]: { 107 | properties?: { 108 | [id: string]: string[]; 109 | }; 110 | }; 111 | } 112 | 113 | function cleanBlocks(blocks: BlocksJson): BlocksPropertyInfo { 114 | const result: BlocksPropertyInfo = {}; 115 | for (const blockName in blocks) { 116 | if (blocks.hasOwnProperty(blockName)) { 117 | const blockInfo = blocks[blockName]; 118 | result[blockName] = {}; 119 | if (!!blockInfo.properties) { 120 | Object.assign(result[blockName], blockInfo.properties); 121 | } 122 | } 123 | } 124 | return result; 125 | } 126 | 127 | const blocksData: BlocksJson = JSON.parse( 128 | (await readFileAsync( 129 | path.join(dataDir, "reports", "blocks.json") 130 | )).toString() 131 | ); 132 | return ["blocks", cleanBlocks(blocksData)]; 133 | } 134 | 135 | //#endregion 136 | -------------------------------------------------------------------------------- /src/data/extractor/download.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-require-imports 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | // @ts-ignore `import *` syntax is broken in this case as requestPromise would be a function 5 | import requestPromise from "request-promise-native"; 6 | 7 | export async function getPathToJar( 8 | tempdir: string, 9 | currentversion: string 10 | ): Promise { 11 | if (!!mcLangSettings.data.customJar) { 12 | return { jarPath: mcLangSettings.data.customJar, version: "" }; 13 | } else { 14 | return downloadJar(currentversion, tempdir); 15 | } 16 | } 17 | 18 | export async function downloadJar( 19 | currentversion: string, 20 | tmpDirName: string 21 | ): Promise { 22 | const versionInfo = await getLatestVersionInfo(); 23 | if (versionInfo.id !== currentversion) { 24 | const singleVersion: SingleVersionInformation = await requestPromise( 25 | versionInfo.url, 26 | { 27 | json: true 28 | } 29 | ).promise(); 30 | const jarPath = path.join( 31 | tmpDirName, 32 | `minecraft-function-${versionInfo.id}.jar` 33 | ); 34 | const requestPromised = requestPromise( 35 | singleVersion.downloads.server.url 36 | ); 37 | requestPromised.pipe(fs.createWriteStream(jarPath)); 38 | await Promise.resolve(requestPromised); 39 | return { jarPath, version: versionInfo.id }; 40 | } else { 41 | return undefined; 42 | } 43 | } 44 | 45 | export interface JarInfo { 46 | jarPath: string; 47 | version: string; 48 | } 49 | 50 | //#region Version Manifest Usage 51 | interface SingleVersionInformation { 52 | downloads: { 53 | server: { 54 | sha1: string; 55 | size: number; 56 | url: string; 57 | }; 58 | }; 59 | } 60 | 61 | async function getLatestVersionInfo(): Promise { 62 | const manifest: VersionsManifest = await requestPromise( 63 | "https://launchermeta.mojang.com/mc/game/version_manifest.json", 64 | { 65 | json: true 66 | } 67 | ).promise(); 68 | const version = findVersion(getVersionId(manifest), manifest); 69 | return version; 70 | } 71 | 72 | function getVersionId(manifest: VersionsManifest): string { 73 | if (mcLangSettings.data.snapshots) { 74 | return manifest.latest.snapshot; 75 | } else { 76 | return manifest.latest.release; 77 | } 78 | } 79 | 80 | function findVersion(version: string, manifest: VersionsManifest): VersionInfo { 81 | return manifest.versions.find( 82 | verInfo => verInfo.id === version 83 | ) as VersionInfo; 84 | } 85 | 86 | interface VersionInfo { 87 | id: string; 88 | releaseTime: string; 89 | time: string; 90 | type: "snapshot" | "release"; 91 | url: string; 92 | } 93 | 94 | interface VersionsManifest { 95 | latest: { 96 | release: string; 97 | snapshot: string; 98 | }; 99 | versions: VersionInfo[]; 100 | } 101 | //#endregion 102 | -------------------------------------------------------------------------------- /src/data/extractor/extract-data.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "child_process"; 2 | import * as path from "path"; 3 | import { promisify } from "util"; 4 | 5 | import { cacheFolder } from "../cache"; 6 | const execFileAsync = promisify(execFile); 7 | 8 | /** 9 | * Get the command used to execute a java version 10 | */ 11 | export async function checkJavaPath(): Promise { 12 | const javaPath = mcLangSettings.data.javaPath || "java"; 13 | try { 14 | await execFileAsync(javaPath, ["-version"], { env: process.env }); 15 | return javaPath; 16 | } catch (error) { 17 | throw new Error( 18 | `Could not find Java executable. Got message: '${error}'` 19 | ); 20 | } 21 | } 22 | 23 | export async function runGenerator( 24 | javapath: string, 25 | tempdir: string, 26 | jarpath: string 27 | ): Promise { 28 | const resultFolder = path.join(tempdir, "generated"); 29 | await execFileAsync( 30 | javapath, 31 | [ 32 | "-cp", 33 | jarpath, 34 | "net.minecraft.data.Main", 35 | "--output", 36 | resultFolder, 37 | "--all" 38 | ], 39 | { 40 | cwd: path.join(cacheFolder, "..") // For log output to go in the extension folder 41 | } 42 | ); 43 | return resultFolder; 44 | } 45 | -------------------------------------------------------------------------------- /src/data/extractor/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { tmpdir } from "os"; 3 | import * as path from "path"; 4 | import { promisify } from "util"; 5 | 6 | import { ReturnHelper } from "../../misc-functions"; 7 | import { ReturnSuccess } from "../../types"; 8 | import { cacheData } from "../cache"; 9 | import { Cacheable } from "../types"; 10 | 11 | import { collectData } from "./collect-data"; 12 | import { getPathToJar } from "./download"; 13 | import { checkJavaPath, runGenerator } from "./extract-data"; 14 | 15 | const mkdtmpAsync = promisify(fs.mkdtemp); 16 | 17 | /** 18 | * Will throw an error if something goes wrong. 19 | * Steps: 20 | * - Check if enabled in settings. ✓ 21 | * - Check versions manifest. Compare with cached if available ✓ 22 | * - At the same time, check if java is installed ✓ 23 | * - Get single version information ✓ 24 | * - Download the jar into a temporary folder ✓ 25 | * - Run the exposed data generator. ✓ 26 | * - Collect the data exposed 27 | * - Blocks and states 28 | * - Items 29 | * - (Entities)? 30 | * - Commands 31 | * - Advancements, recipes, structures, tags, etc 32 | * - Cache that data 33 | * - Return the data 34 | */ 35 | export async function collectGlobalData( 36 | currentversion: string = "" 37 | ): Promise | undefined> { 38 | if (mcLangSettings.data.enabled) { 39 | const javaPath = await checkJavaPath(); 40 | mcLangLog(`Using java at path ${javaPath}`); 41 | const dir = await mkdtmpAsync(path.join(tmpdir(), "mcfunction")); 42 | const jarInfo = await getPathToJar(dir, currentversion); 43 | if (jarInfo) { 44 | mcLangLog(`Running generator`); 45 | const datadir = await runGenerator(javaPath, dir, jarInfo.jarPath); 46 | mcLangLog("Generator Finished, collecting data"); 47 | const helper = new ReturnHelper(); 48 | const data = await collectData(jarInfo.version, datadir); 49 | mcLangLog("Data collected, caching data"); 50 | await cacheData(data.data); 51 | mcLangLog("Caching complete"); 52 | return helper.mergeChain(data).succeed(data.data); 53 | } else { 54 | return undefined; 55 | } 56 | } else { 57 | throw new Error( 58 | "Data Obtainer disabled in settings. To obtain data automatically, please enable it." 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/data/extractor/mapfunctions.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | 3 | import { resourceTypes, ReturnHelper } from "../../misc-functions"; 4 | import { typed_keys } from "../../misc-functions/third_party/typed-keys"; 5 | import { ReturnSuccess } from "../../types"; 6 | import { GlobalData, Resources, WorldInfo } from "../types"; 7 | 8 | export async function runMapFunctions( 9 | resources: Resources, 10 | globalData: GlobalData, 11 | packRoot: string, 12 | localData?: WorldInfo 13 | ): Promise> { 14 | const result: Resources = {}; 15 | const helper = new ReturnHelper(); 16 | const promises: Array> = []; 17 | for (const type of typed_keys(resources)) { 18 | type resourcesType = NonNullable; 19 | const val = (result[type] = [] as resourcesType); 20 | const data = resources[type] as resourcesType; 21 | // tslint:disable-next-line:no-unbound-method We control this function, so we know it won't use the this keyword. 22 | const mapFunction = resourceTypes[type].mapFunction; 23 | if (mapFunction) { 24 | promises.push( 25 | ...data.map(async v => { 26 | const res = await mapFunction( 27 | v, 28 | packRoot, 29 | globalData, 30 | localData 31 | ); 32 | helper.merge(res); 33 | val.push(res.data); 34 | }) 35 | ); 36 | } else { 37 | val.push(...data); 38 | } 39 | } 40 | await Promise.all(promises); 41 | return helper.succeed(result); 42 | } 43 | 44 | export async function mapPacksInfo( 45 | packsInfo: WorldInfo, 46 | global: GlobalData 47 | ): Promise> { 48 | const helper = new ReturnHelper(); 49 | const result: WorldInfo = { ...packsInfo, packs: {} }; 50 | const promises = typed_keys(packsInfo.packs).map(async packID => { 51 | const element = packsInfo.packs[packID]; 52 | const subresult = await runMapFunctions( 53 | element.data, 54 | global, 55 | join(packsInfo.location, element.name), 56 | packsInfo 57 | ); 58 | helper.merge(subresult); 59 | result.packs[packID] = { ...element, data: subresult.data }; 60 | }); 61 | await Promise.all(promises); 62 | return helper.succeed(result); 63 | } 64 | -------------------------------------------------------------------------------- /src/data/lists/criteria.ts: -------------------------------------------------------------------------------- 1 | import { stringArrayToNamespaces } from "../../misc-functions"; 2 | 3 | export const verbatimCriteria = new Set([ 4 | "air", 5 | "armor", 6 | "deathCount", 7 | "dummy", 8 | "food", 9 | "health", 10 | "level", 11 | "playerKillCount", 12 | "totalKillCount", 13 | "trigger", 14 | "xp" 15 | ]); 16 | 17 | export const colorCriteria = ["teamkill.", "killedByTeam."]; 18 | 19 | export const itemCriteria = stringArrayToNamespaces([ 20 | "minecraft:broken", 21 | "minecraft:crafted", 22 | "minecraft:dropped", 23 | "minecraft:picked_up", 24 | "minecraft:used" 25 | ]); 26 | 27 | export const blockCriteria = stringArrayToNamespaces(["minecraft:mined"]); 28 | 29 | export const entityCriteria = stringArrayToNamespaces([ 30 | "minecraft:killed_by", 31 | "minecraft:killed" 32 | ]); 33 | -------------------------------------------------------------------------------- /src/data/lists/item-slot.ts: -------------------------------------------------------------------------------- 1 | export const itemSlots = slotsBuilder(); 2 | 3 | function slotsBuilder(): string[] { 4 | const slots = []; 5 | 6 | slots.push("armor.chest", "armor.feet", "armor.head", "armor.legs"); 7 | 8 | for (let i = 0; i < 54; i++) { 9 | slots.push(`container.${i}`); 10 | } 11 | 12 | for (let i = 0; i < 27; i++) { 13 | slots.push(`enderchest.${i}`); 14 | } 15 | 16 | for (let i = 0; i < 25; i++) { 17 | slots.push(`horse.${i}`); 18 | } 19 | slots.push("horse.armor", "horse.chest", "horse.saddle"); 20 | 21 | for (let i = 0; i < 9; i++) { 22 | slots.push(`hotbar.${i}`); 23 | } 24 | 25 | for (let i = 0; i < 27; i++) { 26 | slots.push(`inventory.${i}`); 27 | } 28 | 29 | for (let i = 0; i < 8; i++) { 30 | slots.push(`villager.${i}`); 31 | } 32 | 33 | slots.push("weapon", "weapon.mainhand", "weapon.offhand"); 34 | return slots; 35 | } 36 | -------------------------------------------------------------------------------- /src/data/lists/scoreboard-slot.ts: -------------------------------------------------------------------------------- 1 | import { COLORS } from "../../colors"; 2 | 3 | export const scoreboardSlots = createSlots(); 4 | 5 | function createSlots(): string[] { 6 | const slots = []; 7 | 8 | slots.push("list", "sidebar", "belowName"); 9 | 10 | for (const s of COLORS) { 11 | slots.push(`sidebar.team.${s}`); 12 | } 13 | return slots; 14 | } 15 | -------------------------------------------------------------------------------- /src/data/lists/statics.ts: -------------------------------------------------------------------------------- 1 | import { COLORS } from "../../colors"; 2 | 3 | export const anchors = ["feet", "eyes"]; 4 | export const operations = ["+=", "-=", "*=", "/=", "%=", "=", ">", "<", "><"]; 5 | export const colors = COLORS; 6 | -------------------------------------------------------------------------------- /src/data/nbt/buffer-stream.ts: -------------------------------------------------------------------------------- 1 | import * as Long from "long"; 2 | 3 | export class BufferStream { 4 | private readonly buf: Buffer; 5 | private index: number; 6 | 7 | public constructor(buffer: Buffer) { 8 | this.index = 0; 9 | this.buf = buffer; 10 | } 11 | 12 | public getByte(): number { 13 | const out = this.buf.readInt8(this.index); 14 | this.index++; 15 | return out; 16 | } 17 | 18 | public getDouble(): number { 19 | const out = this.buf.readDoubleBE(this.index); 20 | this.index += 8; 21 | return out; 22 | } 23 | 24 | public getFloat(): number { 25 | const out = this.buf.readFloatBE(this.index); 26 | this.index += 4; 27 | return out; 28 | } 29 | 30 | public getInt(): number { 31 | const out = this.buf.readInt32BE(this.index); 32 | this.index += 4; 33 | return out; 34 | } 35 | 36 | public getLong(): Long { 37 | const arr = this.buf.subarray(this.index, this.index + 8); 38 | this.index += 8; 39 | return Long.fromBytesBE([...arr]); 40 | } 41 | 42 | public getShort(): number { 43 | const out = this.buf.readInt16BE(this.index); 44 | this.index += 2; 45 | return out; 46 | } 47 | 48 | public getUTF8(): string { 49 | const len = this.getShort(); 50 | const out = this.buf.toString("utf8", this.index, this.index + len); 51 | this.index += len; 52 | return out; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/data/nbt/nbt-cache.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { readFileAsync } from "../../misc-functions"; 4 | import { WorldNBT } from "../types"; 5 | 6 | import { Level, Scoreboard } from "./nbt-types"; 7 | import { parse } from "./parser"; 8 | 9 | export async function loadNBT(worldLoc: string): Promise { 10 | const nbt: WorldNBT = {} as WorldNBT; 11 | 12 | const levelpath = path.resolve(worldLoc, "./level.dat"); 13 | try { 14 | const levelbuf: Buffer = await readFileAsync(levelpath); 15 | nbt.level = await parse(levelbuf); 16 | } catch (e) { 17 | // Level doesn't exist 18 | } 19 | 20 | const scpath = path.resolve(worldLoc, "./data/scoreboard.dat"); 21 | try { 22 | const scoreboardbuf: Buffer = await readFileAsync(scpath); 23 | nbt.scoreboard = await parse(scoreboardbuf); 24 | } catch (e) { 25 | // Scoreboard file doesn't exist 26 | } 27 | 28 | return nbt; 29 | } 30 | -------------------------------------------------------------------------------- /src/data/nbt/parser.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from "util"; 2 | import * as zlib from "zlib"; 3 | 4 | import { BufferStream } from "./buffer-stream"; 5 | 6 | const unzipAsync = promisify(zlib.unzip); 7 | 8 | let tags: Tag; 9 | 10 | type tagparser = (buffer: BufferStream) => T; 11 | 12 | const nbtbyte = (buffer: BufferStream) => buffer.getByte(); 13 | const nbtshort = (buffer: BufferStream) => buffer.getShort(); 14 | const nbtint = (buffer: BufferStream) => buffer.getInt(); 15 | const nbtlong = (buffer: BufferStream) => buffer.getLong(); 16 | const nbtfloat = (buffer: BufferStream) => buffer.getFloat(); 17 | const nbtdouble = (buffer: BufferStream) => buffer.getDouble(); 18 | 19 | const nbtbytearray = (buffer: BufferStream) => { 20 | const len = buffer.getInt(); 21 | const out: number[] = []; 22 | for (let i = 0; i < len; i++) { 23 | out.push(buffer.getByte()); 24 | } 25 | return out; 26 | }; 27 | 28 | const nbtstring = (buffer: BufferStream) => buffer.getUTF8(); 29 | 30 | const nbtlist = (buffer: BufferStream) => { 31 | const id = buffer.getByte(); 32 | const len = buffer.getInt(); 33 | const parser = tags[id]; 34 | const out: any[] = []; 35 | for (let i = 0; i < len; i++) { 36 | out.push(parser(buffer)); 37 | } 38 | return out; 39 | }; 40 | 41 | const nbtcompound = (buffer: BufferStream) => { 42 | let tag: number = buffer.getByte(); 43 | const out: { [key: string]: any } = {}; 44 | while (tag !== 0) { 45 | const name = buffer.getUTF8(); 46 | const parser = tags[tag]; 47 | out[name] = parser(buffer); 48 | tag = buffer.getByte(); 49 | } 50 | return out; 51 | }; 52 | 53 | const nbtintarray = (buffer: BufferStream) => { 54 | const len = buffer.getInt(); 55 | const out: number[] = []; 56 | for (let i = 0; i < len; i++) { 57 | out.push(buffer.getInt()); 58 | } 59 | return out; 60 | }; 61 | 62 | const nbtlongarray = (buffer: BufferStream) => { 63 | const len = buffer.getInt(); 64 | const out: Long[] = []; 65 | for (let i = 0; i < len; i++) { 66 | out.push(buffer.getLong()); 67 | } 68 | return out; 69 | }; 70 | 71 | interface Tag { 72 | [id: number]: tagparser; 73 | } 74 | 75 | tags = { 76 | // Need to redeclare because of TSLint 77 | 1: nbtbyte, 78 | 2: nbtshort, 79 | 3: nbtint, 80 | 4: nbtlong, 81 | 5: nbtfloat, 82 | 6: nbtdouble, 83 | 7: nbtbytearray, 84 | 8: nbtstring, 85 | 9: nbtlist, 86 | 10: nbtcompound, 87 | 11: nbtintarray, 88 | 12: nbtlongarray 89 | }; 90 | 91 | export async function parse( 92 | buffer: Buffer, 93 | named: boolean = true 94 | ): Promise { 95 | let unzipbuf; 96 | try { 97 | unzipbuf = await unzipAsync(buffer); 98 | } catch (e) { 99 | unzipbuf = buffer; 100 | } 101 | const stream = new BufferStream(unzipbuf); 102 | const id = stream.getByte(); 103 | if (named) { 104 | stream.getUTF8(); // Name 105 | } 106 | const parser = tags[id]; 107 | return parser(stream) as T; 108 | } 109 | -------------------------------------------------------------------------------- /src/data/noncached.ts: -------------------------------------------------------------------------------- 1 | import { nbtDocs, NBTNode, ValueList } from "mc-nbt-paths"; 2 | import { SynchronousPromise } from "synchronous-promise"; 3 | import { 4 | getLanguageService, 5 | SchemaRequestService 6 | } from "vscode-json-languageservice"; 7 | 8 | import { NBTDocs, NonCacheable } from "./types"; 9 | 10 | export function loadNBTDocs(): NBTDocs { 11 | const nbtData = new Map(); 12 | Object.keys(nbtDocs).forEach(k => nbtData.set(k, nbtDocs[k])); 13 | return nbtData; 14 | } 15 | const textComponentSchema = 16 | "https://raw.githubusercontent.com/Levertion/minecraft-json-schema/master/java/shared/text_component.json"; 17 | 18 | export async function loadNonCached(): Promise { 19 | const schemas: { [key: string]: string } = { 20 | [textComponentSchema]: JSON.stringify( 21 | // FIXME: parcel breaks require.resolve so we need to use plain require to get the correct path 22 | // tslint:disable-next-line:no-require-imports 23 | require("minecraft-json-schemas/java/shared/text_component") 24 | ) 25 | }; 26 | const schemaRequestService: SchemaRequestService = url => 27 | schemas.hasOwnProperty(url) 28 | ? SynchronousPromise.resolve(schemas[url]) 29 | : SynchronousPromise.reject( 30 | `Schema at url ${url} not supported` 31 | ); 32 | 33 | const jsonService = getLanguageService({ 34 | promiseConstructor: SynchronousPromise, 35 | schemaRequestService 36 | }); 37 | jsonService.configure({ 38 | allowComments: false, 39 | schemas: [ 40 | { 41 | fileMatch: ["text-component.json"], 42 | uri: textComponentSchema 43 | } 44 | ], 45 | validate: true 46 | }); 47 | 48 | return { 49 | jsonService, 50 | nbt_docs: loadNBTDocs() 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | // Create global variables 2 | 3 | // tslint:disable-next-line:no-namespace 4 | declare namespace NodeJS { 5 | interface Global { 6 | mcLangLog: typeof mcLangLog; 7 | mcLangSettings: typeof mcLangSettings; 8 | } 9 | } 10 | /** 11 | * The settings which this server has been run with. 12 | */ 13 | declare const mcLangSettings: McFunctionSettings; 14 | 15 | interface McFunctionSettings extends LocalMcFunctionSettings { 16 | /** 17 | * Settings related to the collection of data. 18 | */ 19 | data: { 20 | /** 21 | * Global Scoped Setting 22 | * 23 | * __Advanced Users Only__ 24 | * 25 | * The path to a custom (server) jar to attempt to extract data from. 26 | * 27 | * This is recommended for when working with mods, 28 | * or past Minecraft versions (AFTER 1.13 Snapshot 18w01a) only. 29 | * 30 | * Note that not all mod parsers will be always supported, and features may change between snapshots. 31 | */ 32 | customJar: string; 33 | /** 34 | * Global Scoped Setting 35 | * 36 | * Whether to enable the automatic Data Collection downloading. 37 | * 38 | * This does not affect the customJar setting. 39 | * 40 | * Enabled by default, but the collector is disabled. 41 | */ 42 | download: boolean; 43 | /** 44 | * Global Scoped Setting 45 | * 46 | * Whether the Data Collection is enabled. 47 | * 48 | * This DOES affect the customJar setting. 49 | * 50 | * Disabled by default. 51 | */ 52 | enabled: boolean; 53 | /** 54 | * Global Scoped Setting 55 | * 56 | * __Advanced Users Only__ 57 | * 58 | * An optional custom path to the java executable (`java`/`java.exe`). 59 | * 60 | * Should be left blank to use the default `java` in PATH. 61 | */ 62 | javaPath: string; // Should be fine if points to javaw, stout is unused insofar. 63 | /** 64 | * Whether or not to use snapshot versions when collecting data. 65 | */ 66 | snapshots: boolean; 67 | }; 68 | /** 69 | * Custom parsers to be used. 70 | * Note that external users of the server should not support this setting 71 | */ 72 | parsers?: { 73 | [name: string]: import("./types").Parser; 74 | }; 75 | /** 76 | * Settings related to information tracking 77 | */ 78 | trace: { 79 | /** 80 | * Whether or not to show output from internal logging messages. 81 | */ 82 | internalLogging: boolean; 83 | }; 84 | /** 85 | * Settings related to translating the server errors. 86 | */ 87 | translation: { 88 | /** 89 | * The code for the language to be used. 90 | */ 91 | lang: string; 92 | }; 93 | } 94 | 95 | interface LocalMcFunctionSettings { 96 | packhandling: { 97 | /** 98 | * Error when there is an invalid extension 99 | */ 100 | errorExtensions: boolean; 101 | /** 102 | * Additional extensions which are allowed 103 | */ 104 | permittedExtensions: string[]; 105 | }; 106 | } 107 | 108 | /** 109 | * Log a message to the console. 110 | * 111 | * This message will always be available 112 | * to the user in the language server output. 113 | */ 114 | declare const mcLangLog: McLogger & InternalLog; 115 | interface McLogger { 116 | /** 117 | * Log to the console a message useful for debugging. 118 | * 119 | * This is disabled or enabled 120 | * based on the "mcfunction.trace.internalLogging" setting 121 | */ 122 | internal(message: string): void; 123 | } 124 | /** 125 | * An internal logging type to allow proper typing information to be used for mcLangLog. 126 | */ 127 | type InternalLog = (message: string) => void; 128 | 129 | interface Dictionary { 130 | [key: string]: T; 131 | } 132 | -------------------------------------------------------------------------------- /src/misc-functions/context.ts: -------------------------------------------------------------------------------- 1 | import { CommandNodePath } from "../data/types"; 2 | 3 | export interface ContextPath { 4 | data: T; 5 | path: CommandNodePath; 6 | } 7 | 8 | export function resolvePaths( 9 | paths: Array>, 10 | argPath: CommandNodePath 11 | ): T | undefined { 12 | for (const path of paths) { 13 | if (stringArrayEqual(argPath, path.path)) { 14 | return path.data; 15 | } 16 | } 17 | return undefined; 18 | } 19 | 20 | export function stringArrayEqual(arr1: string[], arr2: string[]): boolean { 21 | return arr1.length === arr2.length && arr1.every((v, i) => v === arr2[i]); 22 | } 23 | 24 | export function startPaths( 25 | paths: Array>, 26 | argpath: CommandNodePath 27 | ): T | undefined { 28 | let best: [number, T?] = [0, undefined]; 29 | for (const option of paths) { 30 | if ( 31 | option.path.length > best[0] && 32 | option.path.length <= argpath.length && 33 | option.path.every((v, i) => v === argpath[i]) 34 | ) { 35 | best = [option.path.length, option.data]; 36 | } 37 | } 38 | return best[1]; 39 | } 40 | -------------------------------------------------------------------------------- /src/misc-functions/creators.ts: -------------------------------------------------------------------------------- 1 | import { CommandNode, CommandNodePath } from "../data/types"; 2 | import { CommandContext, CommandData, CommandLine, ParserInfo } from "../types"; 3 | 4 | /** 5 | * Build parser info from the data required 6 | */ 7 | export function createParserInfo( 8 | node: CommandNode, 9 | data: CommandData, 10 | path: CommandNodePath, 11 | context: CommandContext, 12 | suggesting: boolean 13 | ): ParserInfo { 14 | const result: ParserInfo = { 15 | context, 16 | data, 17 | node_properties: node.properties || {}, 18 | path, 19 | suggesting 20 | }; 21 | return result; 22 | } 23 | 24 | /** 25 | * Convert a string into CommandLines based on newline characters 26 | */ 27 | export function splitLines(text: string): CommandLine[] { 28 | return createCommandLines(text.split(/\r?\n/)); 29 | } 30 | 31 | /** 32 | * Convert the given string array into a blank CommandLine Array 33 | */ 34 | export function createCommandLines(lines: string[]): CommandLine[] { 35 | const result: CommandLine[] = []; 36 | for (const line of lines) { 37 | result.push({ text: line }); 38 | } 39 | return result; 40 | } 41 | -------------------------------------------------------------------------------- /src/misc-functions/file-errors.ts: -------------------------------------------------------------------------------- 1 | import { MiscInfo } from "../types"; 2 | 3 | export function createExtensionFileError( 4 | filePath: string, 5 | expected: string, 6 | actual: string 7 | ): MiscInfo { 8 | return { 9 | filePath, 10 | group: "extension", 11 | kind: "FileError", 12 | message: `File has incorrect extension: Expected ${expected}, got ${actual}.` 13 | }; 14 | } 15 | 16 | export function createJSONFileError(filePath: string, error: any): MiscInfo { 17 | return { 18 | filePath, 19 | group: "json", 20 | kind: "FileError", 21 | message: `JSON parsing failed: '${error}'` 22 | }; 23 | } 24 | 25 | export function createFileClear(filePath: string, group?: string): MiscInfo { 26 | return { kind: "ClearError", filePath, group }; 27 | } 28 | -------------------------------------------------------------------------------- /src/misc-functions/group-resources.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GlobalData, 3 | MinecraftResource, 4 | NamespacedName, 5 | Resources, 6 | WorldInfo 7 | } from "../data/types"; 8 | import { CommandData } from "../types"; 9 | 10 | import { namespacesEqual } from "./namespace"; 11 | 12 | export function getResourcesofType< 13 | T extends MinecraftResource = MinecraftResource 14 | >(resources: CommandData, type: keyof Resources): T[] { 15 | return getResourcesSplit( 16 | type, 17 | resources.globalData, 18 | resources.localData 19 | ); 20 | } 21 | 22 | export function getResourcesSplit< 23 | T extends MinecraftResource = MinecraftResource 24 | >(type: keyof Resources, globalData: GlobalData, packsInfo?: WorldInfo): T[] { 25 | const results: MinecraftResource[] = []; 26 | const globalResources = globalData.resources[type]; 27 | if (!!globalResources) { 28 | results.push(...globalResources); 29 | } 30 | if (packsInfo) { 31 | for (const packId in packsInfo.packs) { 32 | if (packsInfo.packs.hasOwnProperty(packId)) { 33 | const pack = packsInfo.packs[packId]; 34 | if (pack.data.hasOwnProperty(type)) { 35 | const data = pack.data[type]; 36 | if (!!data) { 37 | results.push(...data); 38 | } 39 | } 40 | } 41 | } 42 | } 43 | return results as T[]; 44 | } 45 | 46 | export function getMatching( 47 | resources: T[], 48 | value: T 49 | ): T[] { 50 | const results: T[] = []; 51 | for (const resource of resources) { 52 | if (namespacesEqual(resource, value)) { 53 | results.push(resource); 54 | } 55 | } 56 | return results; 57 | } 58 | -------------------------------------------------------------------------------- /src/misc-functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./context"; 2 | export * from "./creators"; 3 | export * from "./datapack-folder"; 4 | export * from "./file-errors"; 5 | export * from "./group-resources"; 6 | export * from "./lsp-conversions"; 7 | export * from "./namespace"; 8 | export * from "./node-tree"; 9 | export * from "./promisified-fs"; 10 | export * from "./return-helper"; 11 | export * from "./security"; 12 | export * from "./setup"; 13 | export * from "./translation"; 14 | 15 | export * from "./parsing/namespace"; 16 | export * from "./parsing/nmsp-tag"; 17 | -------------------------------------------------------------------------------- /src/misc-functions/lsp-conversions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Diagnostic, 3 | DidChangeTextDocumentParams, 4 | Range 5 | } from "vscode-languageserver/lib/main"; 6 | 7 | import { CommandError } from "../brigadier/errors"; 8 | import { FunctionInfo } from "../types"; 9 | 10 | import { splitLines } from "./creators"; 11 | import { shouldTranslate } from "./translation"; 12 | 13 | /** 14 | * Turn a command error into a language server diagnostic 15 | */ 16 | export function commandErrorToDiagnostic( 17 | error: CommandError, 18 | line: number 19 | ): Diagnostic { 20 | const range: Range = { 21 | end: { line, character: error.range.end }, 22 | start: { line, character: error.range.start } 23 | }; 24 | // Run Translation stuff on the error? 25 | const text = shouldTranslate() 26 | ? `'${error.text}': Translation is not yet supported` // Translate(error.code) 27 | : error.text; 28 | return Diagnostic.create( 29 | range, 30 | text, 31 | error.severity, 32 | error.code, 33 | "mcfunction" 34 | ); 35 | } 36 | 37 | export function runChanges( 38 | changes: DidChangeTextDocumentParams, 39 | functionInfo: FunctionInfo 40 | ): number[] { 41 | const changed: number[] = []; 42 | for (const change of changes.contentChanges) { 43 | if (!!change.range) { 44 | // Appease the compiler, as the change interface seems to have range optional 45 | const { start, end }: Range = change.range; 46 | const newLineContent = functionInfo.lines[start.line].text 47 | .substring(0, start.character) 48 | .concat( 49 | change.text, 50 | functionInfo.lines[end.line].text.substring(end.character) 51 | ); 52 | const difference = end.line - start.line + 1; 53 | const newLines = splitLines(newLineContent); 54 | functionInfo.lines.splice(start.line, difference, ...newLines); 55 | changed.forEach((v, i) => { 56 | if (v > start.line) { 57 | changed[i] = v - difference + newLines.length; 58 | } 59 | }); 60 | changed.push( 61 | ...Array.from( 62 | new Array(newLines.length), 63 | (_, i) => start.line + i 64 | ) 65 | ); 66 | } 67 | } 68 | const unique = changed.filter( 69 | (value, index, self) => self.indexOf(value) === index 70 | ); 71 | unique.sort((a, b) => a - b); 72 | return unique; 73 | } 74 | -------------------------------------------------------------------------------- /src/misc-functions/namespace.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_NAMESPACE, NAMESPACE } from "../consts"; 2 | import { NamespacedName } from "../data/types"; 3 | 4 | export function namespacesEqual( 5 | first: NamespacedName, 6 | second: NamespacedName 7 | ): boolean { 8 | return namesEqual(first, second) && first.path === second.path; 9 | } 10 | 11 | export function namesEqual( 12 | first: NamespacedName, 13 | second: NamespacedName 14 | ): boolean { 15 | return ( 16 | first.namespace === second.namespace || 17 | (isNamespaceDefault(first) && isNamespaceDefault(second)) 18 | ); 19 | } 20 | 21 | export function isNamespaceDefault(name: NamespacedName): boolean { 22 | return name.namespace === undefined || name.namespace === DEFAULT_NAMESPACE; 23 | } 24 | 25 | export function stringifyNamespace( 26 | namespace: NamespacedName, 27 | seperator: string = NAMESPACE 28 | ): string { 29 | return ( 30 | (namespace.namespace ? namespace.namespace : DEFAULT_NAMESPACE) + 31 | seperator + 32 | namespace.path 33 | ); 34 | } 35 | 36 | /** 37 | * Convert a string into a `NamespacedName`. This should only be called directly on strings which are known to be valid 38 | * The behaviour on invalid strings is to leave the second seperator in the path 39 | */ 40 | export function convertToNamespace( 41 | input: string, 42 | splitChar: string = NAMESPACE 43 | ): NamespacedName { 44 | const index = input.indexOf(splitChar); 45 | if (index >= 0) { 46 | const pathContents = input.substring( 47 | index + splitChar.length, 48 | input.length 49 | ); 50 | // Path contents should not have a : in the contents, however this is to be checked higher up. 51 | // This simplifies using the parsed result when parsing known statics 52 | 53 | // Related: https://bugs.mojang.com/browse/MC-91245 (Fixed) 54 | if (index >= 1) { 55 | return { namespace: input.substring(0, index), path: pathContents }; 56 | } else { 57 | return { path: pathContents }; 58 | } 59 | } else { 60 | return { path: input }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/misc-functions/node-tree.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommandNode, 3 | CommandNodePath, 4 | CommandTree, 5 | MCNode 6 | } from "../data/types"; 7 | 8 | export function followPath>( 9 | tree: MCNode, 10 | path: CommandNodePath 11 | ): MCNode { 12 | // There are no protections here, because if a path is given it should be correct. 13 | let current = tree; 14 | for (const section of path) { 15 | if (!!current.children && !!current.children[section]) { 16 | current = current.children[section]; 17 | } 18 | } 19 | return current; 20 | } 21 | 22 | export function getNextNode( 23 | node: 24 | | CommandNode 25 | | MCNode & { 26 | executable?: boolean; 27 | redirect?: CommandNodePath; 28 | }, // Allow use of node.redirect without a tsignore 29 | nodePath: CommandNodePath, 30 | tree: CommandTree 31 | ): GetNodeResult { 32 | const redirect: CommandNodePath | undefined = node.redirect; 33 | if (!!redirect) { 34 | return { node: followPath(tree, redirect), path: redirect }; 35 | } else { 36 | if (!node.children && !node.executable) { 37 | // In this case either tree is malformed or in `execute run` 38 | // So we just return the entire tree 39 | return { node: tree, path: [] }; 40 | } 41 | return { node, path: nodePath }; 42 | } 43 | } 44 | 45 | interface GetNodeResult { 46 | node: MCNode; 47 | path: CommandNodePath; 48 | } 49 | -------------------------------------------------------------------------------- /src/misc-functions/parsing/nmsp-tag.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind } from "vscode-languageserver/lib/main"; 2 | 3 | import { 4 | buildPath, 5 | convertToNamespace, 6 | getResourcesofType, 7 | namespacesEqual, 8 | ReturnHelper 9 | } from ".."; 10 | import { CommandErrorBuilder } from "../../brigadier/errors"; 11 | import { StringReader } from "../../brigadier/string-reader"; 12 | import { TAG_START } from "../../consts"; 13 | import { 14 | DataResource, 15 | NamespacedName, 16 | Resources, 17 | Tag, 18 | WorldInfo 19 | } from "../../data/types"; 20 | import { CE, ParserInfo, ReturnedInfo, ReturnSuccess } from "../../types"; 21 | 22 | import { 23 | parseNamespace, 24 | parseNamespaceOption, 25 | readNamespaceText 26 | } from "./namespace"; 27 | 28 | export interface TagParseResult { 29 | parsed: NamespacedName; 30 | resolved?: NamespacedName[]; 31 | values?: Array>; 32 | } 33 | 34 | /** 35 | * Parse a namespace or tag. 36 | * Returned: 37 | * - values are the resources which are the exact matches 38 | * - resolved are the lowest level tag members 39 | * - parsed is the literal tag. If parsed exists, but not resolved/values, then it was a non-tag 40 | * - if not successful, if data undefined then parsing failed. 41 | * - if data is a value, then a tag parsed but was unknown 42 | */ 43 | export function parseNamespaceOrTag( 44 | reader: StringReader, 45 | info: ParserInfo, 46 | taghandling: keyof Resources | CommandErrorBuilder 47 | ): ReturnedInfo { 48 | const helper = new ReturnHelper(info); 49 | const start = reader.cursor; 50 | if (reader.peek() === TAG_START) { 51 | reader.skip(); 52 | if (typeof taghandling === "string") { 53 | const tags: Array> = getResourcesofType( 54 | info.data, 55 | taghandling 56 | ); 57 | const parsed = parseNamespaceOption( 58 | reader, 59 | tags, 60 | CompletionItemKind.Folder 61 | ); 62 | if (helper.merge(parsed)) { 63 | const values = parsed.data.values; 64 | const resolved: NamespacedName[] = []; 65 | for (const value of values) { 66 | resolved.push(...getLowestForTag(value, tags)); 67 | } 68 | return helper.succeed({ 69 | parsed: parsed.data.literal, 70 | resolved, 71 | values 72 | }); 73 | } else { 74 | return helper.failWithData(parsed.data); 75 | } 76 | } else { 77 | readNamespaceText(reader); 78 | return helper.fail(taghandling.create(start, reader.cursor)); 79 | } 80 | } else { 81 | if (!reader.canRead() && typeof taghandling === "string") { 82 | helper.addSuggestion( 83 | reader.cursor, 84 | TAG_START, 85 | CompletionItemKind.Operator 86 | ); 87 | } 88 | const parsed = parseNamespace(reader); 89 | if (helper.merge(parsed)) { 90 | return helper.succeed({ parsed: parsed.data }); 91 | } else { 92 | return helper.fail(); 93 | } 94 | } 95 | } 96 | 97 | function getLowestForTag( 98 | tag: DataResource, 99 | options: Array> 100 | ): NamespacedName[] { 101 | if (!tag.data) { 102 | return []; 103 | } 104 | const results: NamespacedName[] = []; 105 | for (const tagMember of tag.data.values) { 106 | if (tagMember[0] === TAG_START) { 107 | const namespace = convertToNamespace(tagMember.substring(1)); 108 | for (const option of options) { 109 | if (namespacesEqual(namespace, option)) { 110 | results.push(...getLowestForTag(option, options)); 111 | } 112 | } 113 | } else { 114 | results.push(convertToNamespace(tagMember)); 115 | } 116 | } 117 | return results; 118 | } 119 | 120 | export function buildTagActions( 121 | tags: Array>, 122 | low: number, 123 | high: number, 124 | type: keyof Resources, 125 | localData?: WorldInfo 126 | ): ReturnSuccess { 127 | const helper = new ReturnHelper(); 128 | for (const resource of tags) { 129 | if (resource.data) { 130 | helper.addActions({ 131 | data: `\`\`\`json 132 | ${JSON.stringify(resource.data, undefined, 4)} 133 | \`\`\``, 134 | high, 135 | low, 136 | type: "hover" 137 | }); 138 | } 139 | if (localData) { 140 | const location = buildPath(resource, localData, type); 141 | if (location) { 142 | helper.addActions({ 143 | data: location, 144 | high, 145 | low, 146 | type: "source" 147 | }); 148 | } 149 | } 150 | } 151 | return helper.succeed(); 152 | } 153 | -------------------------------------------------------------------------------- /src/misc-functions/promisified-fs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { promisify } from "util"; 4 | 5 | import { ReturnedInfo } from "../types"; 6 | 7 | import { createJSONFileError } from "./file-errors"; 8 | import { ReturnHelper } from "./return-helper"; 9 | 10 | export const readFileAsync = promisify(fs.readFile); 11 | export const saveFileAsync = promisify(fs.writeFile); 12 | export const mkdirAsync = promisify(fs.mkdir); 13 | export const readDirAsync = promisify(fs.readdir); 14 | export const statAsync = promisify(fs.stat); 15 | 16 | export async function readJSONRaw(filePath: string): Promise { 17 | const buffer = await readFileAsync(filePath); 18 | return JSON.parse(buffer.toString()); 19 | } 20 | 21 | export async function writeJSON(filepath: string, o: object): Promise { 22 | await saveFileAsync(filepath, JSON.stringify(o, undefined, 4)); 23 | } 24 | 25 | export async function readJSON(filePath: string): Promise> { 26 | const helper = new ReturnHelper(); 27 | let buffer: Buffer; 28 | try { 29 | buffer = await readFileAsync(filePath); 30 | } catch (error) { 31 | mcLangLog(`File at '${filePath}' not available: ${error}`); 32 | return helper.fail(); 33 | } 34 | try { 35 | const result = JSON.parse(buffer.toString()); 36 | return helper.succeed(result); 37 | } catch (e) { 38 | return helper.addMisc(createJSONFileError(filePath, e)).fail(); 39 | } 40 | } 41 | 42 | export async function walkDir(currentPath: string): Promise { 43 | const subFolders: string[] = []; 44 | try { 45 | subFolders.push(...(await readDirAsync(currentPath))); 46 | } catch (error) { 47 | return []; 48 | } 49 | const promises = subFolders.map(async sub => { 50 | try { 51 | const files: string[] = []; 52 | const subFile = path.join(currentPath, sub); 53 | if ((await statAsync(subFile)).isDirectory()) { 54 | files.push(...(await walkDir(subFile))); 55 | } else { 56 | files.push(subFile); 57 | } 58 | return files; 59 | } catch (error) { 60 | return []; 61 | } 62 | }); 63 | const results = await Promise.all(promises); 64 | return ([] as string[]).concat(...results); 65 | } 66 | -------------------------------------------------------------------------------- /src/misc-functions/security.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver/lib/main"; 2 | 3 | import { storeSecurity } from "../data/cache"; 4 | import { WorkspaceSecurity } from "../types"; 5 | 6 | /** 7 | * Check if the given change requires security confirmation 8 | */ 9 | export function securityIssues( 10 | settings: McFunctionSettings, 11 | security: WorkspaceSecurity 12 | ): Array { 13 | const results: Array = []; 14 | if (!!settings.data) { 15 | if (!!settings.data.customJar && security.JarPath !== true) { 16 | results.push("JarPath"); 17 | } 18 | if (!!settings.data.javaPath && security.JavaPath !== true) { 19 | results.push("JavaPath"); 20 | } 21 | } 22 | if (!!settings.parsers /* && security.CustomParsers !== true */) { 23 | /* const names = Object.keys(settings.parsers); 24 | if (names.length > 0) { 25 | results.push("CustomParsers"); 26 | } */ 27 | throw new Error(`Custom parsers are not supported for client implementations. 28 | To request this feature be enabled, open an issue at https://github.com/Levertion/mcfunction-langserver`); 29 | } 30 | return results; 31 | } 32 | 33 | export async function actOnSecurity( 34 | issues: Array, 35 | connection: IConnection, 36 | security: WorkspaceSecurity 37 | ): Promise { 38 | let securityChanged = false; 39 | const resave = async () => { 40 | if (securityChanged) { 41 | await storeSecurity(security); 42 | } 43 | }; 44 | for (const issue of issues) { 45 | const response = await Promise.resolve( 46 | connection.window.showErrorMessage( 47 | `[MCFUNCTION] You have the potentially insecure setting '${issue}' set, but no confirmation has been recieved.`, 48 | { title: "Yes" }, 49 | { title: "No (Stops server)" } 50 | ) 51 | ); 52 | if (!!response && response.title === "Yes") { 53 | security[issue] = true; 54 | securityChanged = true; 55 | } else { 56 | return false; 57 | } 58 | } 59 | await resave(); 60 | return true; 61 | } 62 | -------------------------------------------------------------------------------- /src/misc-functions/setup.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver/lib/main"; 2 | 3 | export function setup_logging(connection: IConnection): void { 4 | const log = (message: string) => { 5 | connection.console.log(message); 6 | }; 7 | // tslint:disable-next-line:prefer-object-spread 8 | global.mcLangLog = Object.assign(log, { 9 | internal: (m: string) => { 10 | if (mcLangSettings.trace.internalLogging) { 11 | log(`[McFunctionInternal] ${m}`); 12 | } 13 | } 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/misc-functions/third_party/child-dir.ts: -------------------------------------------------------------------------------- 1 | import { sep } from "path"; 2 | 3 | /** 4 | * Calculates if a path is inside another path. Taken from: 5 | * 6 | * https://stackoverflow.com/a/42355848/8728461 7 | * @param child The child path 8 | * @param parent The parent path. 9 | */ 10 | export function isChildOf( 11 | child: string, 12 | parent: string, 13 | seperator: string = sep 14 | ): boolean { 15 | if (child === parent) { 16 | return false; 17 | } 18 | const parentTokens = parent.split(seperator).filter(i => i.length); // Has same effect as len>0 19 | const splitChild = child.split(seperator).filter(i => i.length); 20 | return parentTokens.every((t, i) => splitChild[i] === t); 21 | } 22 | -------------------------------------------------------------------------------- /src/misc-functions/third_party/merge-deep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * merge_deep function adapted from: 3 | * 4 | * https://stackoverflow.com/a/34749873/8728461 5 | * 6 | * Originally, non-typescript code by: https://github.com/salakar 7 | */ 8 | 9 | type AnyDict = Dictionary; 10 | 11 | /** 12 | * Simple object check. 13 | * @param item the item 14 | */ 15 | export function isObject(item: any): item is AnyDict { 16 | return item && typeof item === "object" && !Array.isArray(item); 17 | } 18 | 19 | /** 20 | * Deep merge two objects. 21 | */ 22 | export function mergeDeep(target: AnyDict, ...sources: AnyDict[]): AnyDict { 23 | if (!sources.length) { 24 | return target; 25 | } 26 | const source = sources.shift(); 27 | if (isObject(target) && isObject(source)) { 28 | for (const key in source) { 29 | if (isObject(source[key])) { 30 | if (!target[key]) { 31 | Object.assign(target, { [key]: {} }); 32 | } 33 | mergeDeep(target[key], source[key]); 34 | } else { 35 | Object.assign(target, { [key]: source[key] }); 36 | } 37 | } 38 | } 39 | return mergeDeep(target, ...sources); 40 | } 41 | -------------------------------------------------------------------------------- /src/misc-functions/third_party/typed-keys.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the keys of the object in a way friendly to 3 | * the typescript compiler. 4 | * Taken from https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-353494273 5 | * @param o The object to get the keys of. 6 | */ 7 | export function typed_keys(o: O): Array { 8 | return Object.keys(o) as Array; 9 | } 10 | -------------------------------------------------------------------------------- /src/misc-functions/translation.ts: -------------------------------------------------------------------------------- 1 | import { vsprintf } from "sprintf-js"; 2 | 3 | export function shouldTranslate(): boolean { 4 | return mcLangSettings.translation.lang.toLowerCase() !== "en-us"; 5 | } 6 | 7 | export function MCFormat(base: string, ...substitutions: string[]): string { 8 | return vsprintf(base, substitutions); 9 | } 10 | -------------------------------------------------------------------------------- /src/parsers/brigadier/bool.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind } from "vscode-languageserver/lib/main"; 2 | 3 | import { prepareForParser } from "../../misc-functions"; 4 | import { Parser } from "../../types"; 5 | 6 | export const boolParser: Parser = { 7 | kind: CompletionItemKind.Keyword, 8 | parse: (reader, props) => prepareForParser(reader.readBoolean(), props) 9 | }; 10 | -------------------------------------------------------------------------------- /src/parsers/brigadier/double.ts: -------------------------------------------------------------------------------- 1 | import { CommandErrorBuilder } from "../../brigadier/errors"; 2 | import { ReturnHelper } from "../../misc-functions"; 3 | import { Parser } from "../../types"; 4 | 5 | // tslint:disable:binary-expression-operand-order 6 | // Approx 7 | const JAVAMINDOUBLE = -1.8 * 10 ** 308; 8 | const JAVAMAXDOUBLE = 1.8 * 10 ** 308; 9 | // tslint:enable:binary-expression-operand-order 10 | 11 | const DOUBLEEXCEPTIONS = { 12 | TOOBIG: new CommandErrorBuilder( 13 | "argument.double.big", 14 | "Float must not be more than %s, found %s" 15 | ), 16 | TOOSMALL: new CommandErrorBuilder( 17 | "argument.double.low", 18 | "Float must not be less than %s, found %s" 19 | ) 20 | }; 21 | 22 | export const doubleParser: Parser = { 23 | parse: (reader, properties) => { 24 | const helper = new ReturnHelper(properties); 25 | const start = reader.cursor; 26 | const result = reader.readFloat(); 27 | if (!helper.merge(result)) { 28 | return helper.fail(); 29 | } 30 | const maxVal = properties.node_properties.max; 31 | const minVal = properties.node_properties.min; 32 | // See https://stackoverflow.com/a/12957445 33 | const max = Math.min( 34 | typeof maxVal === "number" ? maxVal : JAVAMAXDOUBLE, 35 | JAVAMAXDOUBLE 36 | ); 37 | const min = Math.max( 38 | typeof minVal === "number" ? minVal : JAVAMINDOUBLE, 39 | JAVAMINDOUBLE 40 | ); 41 | if (result.data > max) { 42 | helper.addErrors( 43 | DOUBLEEXCEPTIONS.TOOBIG.create( 44 | start, 45 | reader.cursor, 46 | max.toString(), 47 | result.data.toString() 48 | ) 49 | ); 50 | } 51 | if (result.data < min) { 52 | helper.addErrors( 53 | DOUBLEEXCEPTIONS.TOOSMALL.create( 54 | start, 55 | reader.cursor, 56 | min.toString(), 57 | result.data.toString() 58 | ) 59 | ); 60 | } 61 | return helper.succeed(); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/parsers/brigadier/float.ts: -------------------------------------------------------------------------------- 1 | import { CommandErrorBuilder } from "../../brigadier/errors"; 2 | import { ReturnHelper } from "../../misc-functions"; 3 | import { Parser } from "../../types"; 4 | 5 | // tslint:disable:binary-expression-operand-order 6 | // Approx 7 | const JAVAMINFLOAT = -3.4 * 10 ** 38; 8 | const JAVAMAXFLOAT = 3.4 * 10 ** 38; 9 | // tslint:enable:binary-expression-operand-order 10 | 11 | const FLOATEXCEPTIONS = { 12 | TOOBIG: new CommandErrorBuilder( 13 | "argument.float.big", 14 | "Float must not be more than %s, found %s" 15 | ), 16 | TOOSMALL: new CommandErrorBuilder( 17 | "argument.float.low", 18 | "Float must not be less than %s, found %s" 19 | ) 20 | }; 21 | 22 | export const floatParser: Parser = { 23 | parse: (reader, properties) => { 24 | const helper = new ReturnHelper(properties); 25 | const start = reader.cursor; 26 | const result = reader.readFloat(); 27 | if (!helper.merge(result)) { 28 | return helper.fail(); 29 | } 30 | const maxVal = properties.node_properties.max; 31 | const minVal = properties.node_properties.min; 32 | // See https://stackoverflow.com/a/12957445 33 | const max = Math.min( 34 | typeof maxVal === "number" ? maxVal : JAVAMAXFLOAT, 35 | JAVAMAXFLOAT 36 | ); 37 | const min = Math.max( 38 | typeof minVal === "number" ? minVal : JAVAMINFLOAT, 39 | JAVAMINFLOAT 40 | ); 41 | if (result.data > max) { 42 | helper.addErrors( 43 | FLOATEXCEPTIONS.TOOBIG.create( 44 | start, 45 | reader.cursor, 46 | max.toString(), 47 | result.data.toString() 48 | ) 49 | ); 50 | } 51 | if (result.data < min) { 52 | helper.addErrors( 53 | FLOATEXCEPTIONS.TOOSMALL.create( 54 | start, 55 | reader.cursor, 56 | min.toString(), 57 | result.data.toString() 58 | ) 59 | ); 60 | } 61 | return helper.succeed(); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/parsers/brigadier/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bool"; 2 | export * from "./double"; 3 | export * from "./float"; 4 | export * from "./integer"; 5 | export * from "./string"; 6 | -------------------------------------------------------------------------------- /src/parsers/brigadier/integer.ts: -------------------------------------------------------------------------------- 1 | import { CommandErrorBuilder } from "../../brigadier/errors"; 2 | import { JAVAMAXINT, JAVAMININT } from "../../consts"; 3 | import { ReturnHelper } from "../../misc-functions"; 4 | import { Parser } from "../../types"; 5 | 6 | const INTEGEREXCEPTIONS = { 7 | TOOBIG: new CommandErrorBuilder( 8 | "argument.integer.big", 9 | "Integer must not be more than %s, found %s" 10 | ), 11 | TOOSMALL: new CommandErrorBuilder( 12 | "argument.integer.low", 13 | "Integer must not be less than %s, found %s" 14 | ) 15 | }; 16 | 17 | export const intParser: Parser = { 18 | parse: (reader, properties) => { 19 | const helper = new ReturnHelper(properties); 20 | const start = reader.cursor; 21 | const result = reader.readInt(); 22 | if (!helper.merge(result)) { 23 | return helper.fail(); 24 | } 25 | const maxVal = properties.node_properties.max; 26 | const minVal = properties.node_properties.min; 27 | // See https://stackoverflow.com/a/12957445 28 | const max = Math.min( 29 | typeof maxVal === "number" ? maxVal : JAVAMAXINT, 30 | JAVAMAXINT 31 | ); 32 | const min = Math.max( 33 | typeof minVal === "number" ? minVal : JAVAMININT, 34 | JAVAMININT 35 | ); 36 | if (result.data > max) { 37 | helper.addErrors( 38 | INTEGEREXCEPTIONS.TOOBIG.create( 39 | start, 40 | reader.cursor, 41 | max.toString(), 42 | result.data.toString() 43 | ) 44 | ); 45 | } 46 | if (result.data < min) { 47 | helper.addErrors( 48 | INTEGEREXCEPTIONS.TOOSMALL.create( 49 | start, 50 | reader.cursor, 51 | min.toString(), 52 | result.data.toString() 53 | ) 54 | ); 55 | } 56 | return helper.succeed(); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/parsers/brigadier/string.ts: -------------------------------------------------------------------------------- 1 | import { ReturnHelper } from "../../misc-functions"; 2 | import { Parser } from "../../types"; 3 | 4 | export const stringParser: Parser = { 5 | parse: (reader, properties) => { 6 | const helper = new ReturnHelper(properties); 7 | switch (properties.node_properties.type) { 8 | case "greedy": 9 | reader.cursor = reader.string.length; 10 | return helper.succeed(); 11 | case "word": 12 | reader.readUnquotedString(); 13 | return helper.succeed(); 14 | default: 15 | if (helper.merge(reader.readString())) { 16 | return helper.succeed(); 17 | } else { 18 | return helper.fail(); 19 | } 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/parsers/get-parser.ts: -------------------------------------------------------------------------------- 1 | import { CommandNode } from "../data/types"; 2 | import { Parser } from "../types"; 3 | 4 | import * as brigadierParsers from "./brigadier"; 5 | import { literalParser } from "./literal"; 6 | import * as blockParsers from "./minecraft/block"; 7 | import { jsonParser } from "./minecraft/component"; 8 | import * as coordParsers from "./minecraft/coordinates"; 9 | import * as entityParser from "./minecraft/entity"; 10 | import * as itemParsers from "./minecraft/item"; 11 | import * as listParsers from "./minecraft/lists"; 12 | import { messageParser } from "./minecraft/message"; 13 | import * as namespaceParsers from "./minecraft/namespace-list"; 14 | import { pathParser } from "./minecraft/nbt-path"; 15 | import { nbtParser } from "./minecraft/nbt/nbt"; 16 | import { intRange } from "./minecraft/range"; 17 | import { functionParser, resourceParser } from "./minecraft/resources"; 18 | import { 19 | criteriaParser, 20 | objectiveParser, 21 | teamParser 22 | } from "./minecraft/scoreboard"; 23 | import { swizzleParer } from "./minecraft/swizzle"; 24 | import { timeParser } from "./minecraft/time"; 25 | 26 | /** 27 | * Incomplete: 28 | * https://github.com/Levertion/mcfunction-langserver/projects/1 29 | */ 30 | const implementedParsers: { [id: string]: Parser } = { 31 | "brigadier:bool": brigadierParsers.boolParser, 32 | "brigadier:double": brigadierParsers.doubleParser, 33 | "brigadier:float": brigadierParsers.floatParser, 34 | "brigadier:integer": brigadierParsers.intParser, 35 | "brigadier:string": brigadierParsers.stringParser, 36 | "minecraft:block_pos": coordParsers.blockPos, 37 | "minecraft:block_predicate": blockParsers.predicateParser, 38 | "minecraft:block_state": blockParsers.stateParser, 39 | "minecraft:color": listParsers.colorParser, 40 | "minecraft:column_pos": coordParsers.columnPos, 41 | "minecraft:component": jsonParser, 42 | "minecraft:dimension": namespaceParsers.dimensionParser, 43 | "minecraft:entity": entityParser.entity, 44 | "minecraft:entity_anchor": listParsers.entityAnchorParser, 45 | "minecraft:entity_summon": namespaceParsers.summonParser, 46 | "minecraft:function": functionParser, 47 | "minecraft:game_profile": entityParser.gameProfile, 48 | "minecraft:int_range": intRange, 49 | "minecraft:item_enchantment": namespaceParsers.enchantmentParser, 50 | "minecraft:item_predicate": itemParsers.predicate, 51 | "minecraft:item_slot": listParsers.itemSlotParser, 52 | "minecraft:item_stack": itemParsers.stack, 53 | "minecraft:message": messageParser, 54 | "minecraft:mob_effect": namespaceParsers.mobEffectParser, 55 | "minecraft:nbt_compound_tag": nbtParser, 56 | "minecraft:nbt_path": pathParser, 57 | "minecraft:nbt_tag": nbtParser, 58 | "minecraft:objective": objectiveParser, 59 | "minecraft:objective_criteria": criteriaParser, 60 | "minecraft:operation": listParsers.operationParser, 61 | "minecraft:particle": namespaceParsers.particleParser, 62 | "minecraft:resource_location": resourceParser, 63 | "minecraft:rotation": coordParsers.rotation, 64 | "minecraft:score_holder": entityParser.scoreHolder, 65 | "minecraft:scoreboard_slot": listParsers.scoreBoardSlotParser, 66 | "minecraft:swizzle": swizzleParer, 67 | "minecraft:team": teamParser, 68 | "minecraft:time": timeParser, 69 | "minecraft:vec2": coordParsers.vec2, 70 | "minecraft:vec3": coordParsers.vec3 71 | }; 72 | 73 | export function getParser(node: CommandNode): Parser | undefined { 74 | switch (node.type) { 75 | case "literal": 76 | return literalParser; 77 | case "argument": 78 | if (!!node.parser) { 79 | return getArgParser(node.parser); 80 | } 81 | break; 82 | default: 83 | } 84 | return undefined; 85 | } 86 | 87 | function getArgParser(id: string): Parser | undefined { 88 | if ( 89 | !!global.mcLangSettings && 90 | !!global.mcLangSettings.parsers && 91 | global.mcLangSettings.parsers.hasOwnProperty(id) 92 | ) { 93 | try { 94 | return global.mcLangSettings.parsers[id]; 95 | } catch (_) { 96 | mcLangLog( 97 | `${global.mcLangSettings.parsers[id]} could not be loaded` 98 | ); 99 | } 100 | } 101 | if (implementedParsers.hasOwnProperty(id)) { 102 | return implementedParsers[id]; 103 | } 104 | mcLangLog(`Argument with parser id ${id} has no associated parser. 105 | Please consider reporting this at https://github.com/Levertion/mcfunction-language-server/issues`); 106 | return undefined; 107 | } 108 | -------------------------------------------------------------------------------- /src/parsers/literal.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind } from "vscode-languageserver/lib/main"; 2 | 3 | import { ReturnHelper } from "../misc-functions"; 4 | import { Parser } from "../types"; 5 | 6 | export const literalParser: Parser = { 7 | kind: CompletionItemKind.Method, 8 | parse: (reader, properties) => { 9 | const helper = new ReturnHelper(properties); 10 | const begin = reader.cursor; 11 | const literal = properties.path[properties.path.length - 1]; 12 | if ( 13 | properties.suggesting && 14 | literal.startsWith(reader.getRemaining()) 15 | ) { 16 | helper.addSuggestions(literal); 17 | } 18 | if (reader.canRead(literal.length)) { 19 | const end = begin + literal.length; 20 | if (reader.string.substring(begin, end) === literal) { 21 | reader.cursor = end; 22 | if (reader.peek() === " " || !reader.canRead()) { 23 | return helper.succeed(); 24 | } 25 | } 26 | } 27 | return helper.fail(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/parsers/minecraft/component.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from "vscode-json-languageservice"; 2 | import { DiagnosticSeverity } from "vscode-languageserver"; 3 | 4 | import { CommandError } from "../../brigadier/errors"; 5 | import { ReturnHelper } from "../../misc-functions"; 6 | import { Parser } from "../../types"; 7 | 8 | export const jsonParser: Parser = { 9 | parse: (reader, info) => { 10 | const helper = new ReturnHelper(); 11 | const remaining = reader.getRemaining(); 12 | const start = reader.cursor; 13 | reader.cursor = reader.string.length; 14 | const text: TextDocument = { 15 | getText: () => remaining, 16 | languageId: "json", 17 | lineCount: 1, 18 | offsetAt: pos => pos.character, 19 | positionAt: offset => ({ line: 0, character: offset }), 20 | uri: "file://text-component.json", 21 | version: 0 22 | }; 23 | const service = info.data.globalData.jsonService; 24 | // tslint:disable-next-line:no-inferred-empty-object-type 25 | const json = service.parseJSONDocument(text); 26 | service.doValidation(text, json).then(diagnostics => { 27 | /* Because we use SynchronousPromise this is called before the next statement runs*/ 28 | helper.addErrors( 29 | ...diagnostics.map(diag => ({ 30 | code: typeof diag.code === "string" ? diag.code : "json", 31 | range: { 32 | end: start + diag.range.end.character, 33 | start: start + diag.range.start.character 34 | }, 35 | severity: diag.severity || DiagnosticSeverity.Error, 36 | text: diag.message 37 | })) 38 | ); 39 | }); 40 | service 41 | .doComplete(text, { line: 0, character: remaining.length }, json) 42 | .then(completionList => { 43 | if (completionList) { 44 | completionList.items.forEach(item => { 45 | if (item.textEdit) { 46 | helper.addSuggestions({ 47 | description: item.documentation, 48 | insertTextFormat: item.insertTextFormat, 49 | kind: item.kind, 50 | label: item.label, 51 | start: 52 | start + item.textEdit.range.start.character, 53 | text: item.textEdit.newText.replace( 54 | /\s*\n\s*/g, 55 | "" 56 | ) 57 | }); 58 | } else { 59 | helper.addSuggestions({ 60 | description: item.documentation, 61 | insertTextFormat: item.insertTextFormat, 62 | kind: item.kind, 63 | label: item.label, 64 | start: reader.cursor, 65 | text: item.label.replace(/\s*\n\s*/g, "") 66 | }); 67 | } 68 | }); 69 | } 70 | }); 71 | helper.addActions({ 72 | data: { 73 | json, 74 | text 75 | }, 76 | high: reader.cursor, 77 | low: start, 78 | type: "json" 79 | }); 80 | return helper.succeed(); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/parsers/minecraft/item.ts: -------------------------------------------------------------------------------- 1 | import { CommandErrorBuilder } from "../../brigadier/errors"; 2 | import { StringReader } from "../../brigadier/string-reader"; 3 | import { 4 | buildTagActions, 5 | namespaceSuggestionString, 6 | parseNamespaceOrTag, 7 | ReturnHelper, 8 | stringifyNamespace 9 | } from "../../misc-functions"; 10 | import { Parser, ParserInfo, ReturnedInfo } from "../../types"; 11 | 12 | import { validateParse } from "./nbt/nbt"; 13 | 14 | const NOTAG = new CommandErrorBuilder( 15 | "argument.item.tag.disallowed", 16 | "Tags aren't allowed here, only actual items" 17 | ); 18 | 19 | const UNKNOWNTAG = new CommandErrorBuilder( 20 | "arguments.item.tag.unknown", 21 | "Unknown item tag '%s'" 22 | ); 23 | 24 | const UNKNOWNITEM = new CommandErrorBuilder( 25 | "argument.item.id.invalid", 26 | "Unknown item '%s'" 27 | ); 28 | 29 | export class ItemParser implements Parser { 30 | private readonly useTags: boolean; 31 | 32 | public constructor(useTags: boolean) { 33 | this.useTags = useTags; 34 | } 35 | 36 | public parse( 37 | reader: StringReader, 38 | properties: ParserInfo 39 | ): ReturnedInfo { 40 | const helper = new ReturnHelper(properties); 41 | const start = reader.cursor; 42 | const parsed = parseNamespaceOrTag( 43 | reader, 44 | properties, 45 | this.useTags ? "item_tags" : NOTAG 46 | ); 47 | if (helper.merge(parsed)) { 48 | const items: string[] = []; 49 | if (parsed.data.resolved && parsed.data.values) { 50 | helper.merge( 51 | buildTagActions( 52 | parsed.data.values, 53 | start + 1, 54 | reader.cursor, 55 | "item_tags", 56 | properties.data.localData 57 | ) 58 | ); 59 | parsed.data.values.forEach(v => { 60 | items.push(...(v.data || { values: [] }).values); 61 | }); 62 | } else { 63 | if (properties.suggesting && !reader.canRead()) { 64 | helper.addSuggestions( 65 | ...namespaceSuggestionString( 66 | [ 67 | ...properties.data.globalData.registries[ 68 | "minecraft:item" 69 | ] 70 | ], 71 | parsed.data.parsed, 72 | start 73 | ) 74 | ); 75 | } 76 | const name = stringifyNamespace(parsed.data.parsed); 77 | if ( 78 | !properties.data.globalData.registries[ 79 | "minecraft:item" 80 | ].has(name) 81 | ) { 82 | helper.addErrors( 83 | UNKNOWNITEM.create(start, reader.cursor, name) 84 | ); 85 | } 86 | items.push(name); 87 | } 88 | if (reader.peek() === "{") { 89 | const nbt = validateParse(reader, properties, { 90 | ids: items, 91 | kind: "item" 92 | }); 93 | helper.merge(nbt); 94 | } else { 95 | helper.addSuggestion(reader.cursor, "{"); 96 | } 97 | } else { 98 | if (parsed.data) { 99 | helper.addErrors( 100 | UNKNOWNTAG.create( 101 | start, 102 | reader.cursor, 103 | stringifyNamespace(parsed.data) 104 | ) 105 | ); 106 | if (reader.peek() === "{") { 107 | const nbt = validateParse(reader, properties, { 108 | ids: "none", 109 | kind: "item" 110 | }); 111 | helper.merge(nbt); 112 | } else { 113 | helper.addSuggestion(reader.cursor, "{"); 114 | } 115 | } else { 116 | return helper.fail(); 117 | } 118 | } 119 | return helper.succeed(); 120 | } 121 | } 122 | 123 | export const stack = new ItemParser(false); 124 | export const predicate = new ItemParser(true); 125 | -------------------------------------------------------------------------------- /src/parsers/minecraft/lists.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind } from "vscode-languageserver"; 2 | 3 | import { CommandErrorBuilder } from "../../brigadier/errors"; 4 | import { StringReader } from "../../brigadier/string-reader"; 5 | import { COLORS } from "../../colors"; 6 | import { NONWHITESPACE } from "../../consts"; 7 | import { itemSlots } from "../../data/lists/item-slot"; 8 | import { scoreboardSlots } from "../../data/lists/scoreboard-slot"; 9 | import { anchors, operations } from "../../data/lists/statics"; 10 | import { ReturnHelper } from "../../misc-functions"; 11 | import { Parser, ParserInfo, ReturnedInfo } from "../../types"; 12 | 13 | export class ListParser implements Parser { 14 | private readonly error: CommandErrorBuilder; 15 | private readonly options: string[]; 16 | private readonly regex: RegExp; 17 | 18 | public constructor( 19 | options: string[], 20 | err: CommandErrorBuilder, 21 | regex: RegExp = StringReader.charAllowedInUnquotedString 22 | ) { 23 | this.options = options; 24 | this.error = err; 25 | this.regex = regex; 26 | } 27 | 28 | public parse( 29 | reader: StringReader, 30 | info: ParserInfo 31 | ): ReturnedInfo { 32 | const start = reader.cursor; 33 | const helper = new ReturnHelper(info); 34 | const optResult = reader.readOption( 35 | this.options, 36 | { 37 | quote: false, 38 | unquoted: this.regex 39 | }, 40 | CompletionItemKind.EnumMember 41 | ); 42 | if (helper.merge(optResult)) { 43 | return helper.succeed(); 44 | } else { 45 | return helper.fail( 46 | this.error.create(start, reader.cursor, optResult.data || "") 47 | ); 48 | } 49 | } 50 | } 51 | 52 | const colorError = new CommandErrorBuilder( 53 | "argument.color.invalid", 54 | "Unknown color '%s'" 55 | ); 56 | export const colorParser = new ListParser(COLORS, colorError); 57 | 58 | const entityAnchorError = new CommandErrorBuilder( 59 | "argument.anchor.invalid", 60 | "Invalid entity anchor position %s" 61 | ); 62 | export const entityAnchorParser = new ListParser(anchors, entityAnchorError); 63 | 64 | const slotError = new CommandErrorBuilder("slot.unknown", "Unknown slot '%s'"); 65 | export const itemSlotParser = new ListParser(itemSlots, slotError); 66 | 67 | const operationError = new CommandErrorBuilder( 68 | "arguments.operation.invalid", 69 | "Invalid operation" 70 | ); 71 | export const operationParser = new ListParser( 72 | operations, 73 | operationError, 74 | NONWHITESPACE 75 | ); 76 | 77 | const scoreboardSlotError = new CommandErrorBuilder( 78 | "argument.scoreboardDisplaySlot.invalid", 79 | "Unknown display slot '%s'" 80 | ); 81 | export const scoreBoardSlotParser = new ListParser( 82 | scoreboardSlots, 83 | scoreboardSlotError 84 | ); 85 | -------------------------------------------------------------------------------- /src/parsers/minecraft/message.ts: -------------------------------------------------------------------------------- 1 | import { StringReader } from "../../brigadier/string-reader"; 2 | import { ReturnHelper } from "../../misc-functions"; 3 | import { Parser } from "../../types"; 4 | 5 | export const messageParser: Parser = { 6 | parse: (reader: StringReader) => { 7 | reader.cursor = reader.getTotalLength(); 8 | // tslint:disable:helper-return 9 | return new ReturnHelper().succeed(); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/parsers/minecraft/namespace-list.ts: -------------------------------------------------------------------------------- 1 | import { CommandErrorBuilder } from "../../brigadier/errors"; 2 | import { StringReader } from "../../brigadier/string-reader"; 3 | import { NamespacedName, RegistryNames } from "../../data/types"; 4 | import { 5 | parseNamespaceOption, 6 | ReturnHelper, 7 | stringArrayToNamespaces, 8 | stringifyNamespace 9 | } from "../../misc-functions"; 10 | import { CommandContext, Parser, ParserInfo, ReturnedInfo } from "../../types"; 11 | 12 | export class NamespaceListParser implements Parser { 13 | private readonly error: CommandErrorBuilder; 14 | private readonly registryType: RegistryNames; 15 | private readonly resultFunction?: ( 16 | context: CommandContext, 17 | result: NamespacedName[] 18 | ) => void; 19 | public constructor( 20 | registryType: RegistryNames, 21 | errorBuilder: CommandErrorBuilder, 22 | context?: NamespaceListParser["resultFunction"] 23 | ) { 24 | this.registryType = registryType; 25 | this.error = errorBuilder; 26 | this.resultFunction = context; 27 | } 28 | public parse( 29 | reader: StringReader, 30 | info: ParserInfo 31 | ): ReturnedInfo { 32 | const helper = new ReturnHelper(info); 33 | const start = reader.cursor; 34 | const result = parseNamespaceOption( 35 | reader, 36 | stringArrayToNamespaces([ 37 | ...info.data.globalData.registries[this.registryType] 38 | ]) 39 | ); 40 | if (helper.merge(result)) { 41 | if (this.resultFunction) { 42 | this.resultFunction(info.context, result.data.values); 43 | return helper.succeed(); 44 | } else { 45 | return helper.succeed(); 46 | } 47 | } else { 48 | if (result.data) { 49 | return helper 50 | .addErrors( 51 | this.error.create( 52 | start, 53 | reader.cursor, 54 | stringifyNamespace(result.data) 55 | ) 56 | ) 57 | .succeed(); 58 | } else { 59 | return helper.fail(); 60 | } 61 | } 62 | } 63 | } 64 | 65 | export const summonError = new CommandErrorBuilder( 66 | "entity.notFound", 67 | "Unknown entity: %s" 68 | ); 69 | export const summonParser = new NamespaceListParser( 70 | "minecraft:entity_type", 71 | summonError, 72 | (context, ids) => (context.otherEntity = { ids }) 73 | ); 74 | 75 | const enchantmentError = new CommandErrorBuilder( 76 | "enchantment.unknown", 77 | "Unknown enchantment: %s" 78 | ); 79 | export const enchantmentParser = new NamespaceListParser( 80 | "minecraft:enchantment", 81 | enchantmentError 82 | ); 83 | 84 | const mobEffectError = new CommandErrorBuilder( 85 | "effect.effectNotFound", 86 | "Unknown effect: %s" 87 | ); 88 | export const mobEffectParser = new NamespaceListParser( 89 | "minecraft:mob_effect", 90 | mobEffectError 91 | ); 92 | 93 | const particleError = new CommandErrorBuilder( 94 | "particle.notFound", 95 | "Unknown particle: %s" 96 | ); 97 | export const particleParser = new NamespaceListParser( 98 | "minecraft:particle_type", 99 | particleError 100 | ); 101 | 102 | const dimensionError = new CommandErrorBuilder( 103 | "argument.dimension.invalid", 104 | "Unknown dimension: '%s'" 105 | ); 106 | 107 | export const dimensionParser = new NamespaceListParser( 108 | "minecraft:dimension_type", 109 | dimensionError 110 | ); 111 | -------------------------------------------------------------------------------- /src/parsers/minecraft/nbt/doc-walker-func.ts: -------------------------------------------------------------------------------- 1 | import { FunctionNode } from "mc-nbt-paths"; 2 | import * as path from "path"; 3 | import { sprintf } from "sprintf-js"; 4 | 5 | import { NBTTag } from "./tag/nbt-tag"; 6 | import { NBTTagString } from "./tag/string-tag"; 7 | import { getNBTTagFromTree } from "./util/doc-walker-util"; 8 | 9 | interface PathFunctions { 10 | [key: string]: PathFunc; 11 | } 12 | 13 | interface SuggestFuncs { 14 | [key: string]: SuggestFunc; 15 | } 16 | 17 | type PathFunc = ( 18 | parsed: NBTTag | undefined, 19 | nbtPath: string[], 20 | node: FunctionNode, 21 | args: any 22 | ) => string; 23 | type SuggestFunc = (func: string, args: any) => string[]; 24 | 25 | const pathsFuncs: PathFunctions = { 26 | insertStringNBT 27 | }; 28 | 29 | export function runNodeFunction( 30 | nbtPath: string[], 31 | node: FunctionNode, 32 | parsed: NBTTag | undefined 33 | ): string { 34 | return pathsFuncs[node.function.id]( 35 | parsed, 36 | nbtPath, 37 | node, 38 | node.function.params 39 | ); 40 | } 41 | 42 | const suggestFuncs: SuggestFuncs = {}; 43 | 44 | interface InsertStringNBTArgs { 45 | default: string; 46 | ref: string; 47 | tag_path: string; 48 | } 49 | 50 | function insertStringNBT( 51 | parsed: NBTTag | undefined, 52 | nbtPath: string[], 53 | _: FunctionNode, 54 | args: InsertStringNBTArgs 55 | ): string { 56 | if (!parsed) { 57 | return args.default; 58 | } 59 | const newRef = path.posix 60 | .join(path.dirname(nbtPath.join("/")), args.tag_path) 61 | .split("/"); 62 | const out = getNBTTagFromTree(parsed, newRef); 63 | return !out || !(out instanceof NBTTagString) 64 | ? args.default 65 | : sprintf(args.ref, out.getValue()); 66 | } 67 | 68 | // Suggest function 69 | 70 | export function runSuggestFunction(func: string, args: any): string[] { 71 | return suggestFuncs[func](func, args); 72 | } 73 | -------------------------------------------------------------------------------- /src/parsers/minecraft/nbt/tag-parser.ts: -------------------------------------------------------------------------------- 1 | import { StringReader } from "../../../brigadier/string-reader"; 2 | import { ReturnHelper } from "../../../misc-functions"; 3 | import { CE, ReturnedInfo } from "../../../types"; 4 | 5 | import { NBTTagCompound } from "./tag/compound-tag"; 6 | import { NBTTagList } from "./tag/list-tag"; 7 | import { NBTTag, ParseReturn } from "./tag/nbt-tag"; 8 | import { NBTTagNumber } from "./tag/number"; 9 | import { NBTTagString } from "./tag/string-tag"; 10 | import { NBTTagTypedList } from "./tag/typed-list-tag"; 11 | import { Correctness } from "./util/nbt-util"; 12 | 13 | const parsers: Array NBTTag> = [ 14 | NBTTagNumber, 15 | NBTTagTypedList, 16 | NBTTagCompound, 17 | NBTTagList, 18 | NBTTagString 19 | ]; 20 | 21 | export type AnyTagReturn = ReturnedInfo; 22 | 23 | export interface CorrectInfo { 24 | correctness: Correctness; 25 | tag: NBTTag; 26 | } 27 | export function parseAnyNBTTag( 28 | reader: StringReader, 29 | path: string[] 30 | ): ReturnedInfo { 31 | let info: CorrectInfo | undefined; 32 | let last: ParseReturn | undefined; 33 | const helper = new ReturnHelper(); 34 | const start = reader.cursor; 35 | for (const parserContructor of parsers) { 36 | reader.cursor = start; 37 | const tag = new parserContructor(path); 38 | const out = tag.parse(reader); 39 | if ( 40 | out.data === Correctness.CERTAIN || 41 | out.data > ((info && info.correctness) || Correctness.NO) 42 | ) { 43 | info = { correctness: out.data, tag }; 44 | last = out; 45 | } 46 | if (out.data === Correctness.CERTAIN) { 47 | break; 48 | } 49 | } 50 | // Maybe add could not parse nbt tag error 51 | if (info === undefined || last === undefined) { 52 | return helper.fail(); 53 | } 54 | reader.cursor = info.tag.getRange().end; 55 | if (helper.merge(last)) { 56 | return helper.succeed(info); 57 | } else { 58 | return helper.failWithData(info); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/parsers/minecraft/nbt/tag/list-tag.ts: -------------------------------------------------------------------------------- 1 | import { ListNode } from "mc-nbt-paths"; 2 | 3 | import { StringReader } from "../../../../brigadier/string-reader"; 4 | import { ReturnHelper } from "../../../../misc-functions"; 5 | import { emptyRange } from "../../../../test/blanks"; 6 | import { LineRange, ReturnSuccess } from "../../../../types"; 7 | import { NodeInfo } from "../util/doc-walker-util"; 8 | import { Correctness, LIST_START } from "../util/nbt-util"; 9 | import { NBTWalker } from "../walker"; 10 | 11 | import { BaseList } from "./lists"; 12 | import { ParseReturn } from "./nbt-tag"; 13 | 14 | export class NBTTagList extends BaseList { 15 | public tagType: "list" = "list"; 16 | // The open square bracket 17 | protected start: LineRange = emptyRange(); 18 | 19 | public validate( 20 | anyInfo: NodeInfo, 21 | walker: NBTWalker 22 | ): ReturnSuccess { 23 | const helper = new ReturnHelper(); 24 | const result = this.sameType(anyInfo); 25 | if (!helper.merge(result)) { 26 | return helper.succeed(); 27 | } 28 | const info = anyInfo as NodeInfo; 29 | helper.merge(this.validateWith(info, walker.getItem(info), walker)); 30 | return helper.succeed(); 31 | } 32 | 33 | protected readTag(reader: StringReader): ParseReturn { 34 | const helper = new ReturnHelper(); 35 | const start = reader.cursor; 36 | if (!helper.merge(reader.expect(LIST_START))) { 37 | return helper.failWithData(Correctness.NO); 38 | } 39 | reader.skipWhitespace(); 40 | this.start = { start, end: reader.cursor }; 41 | const result = this.parseInner(reader); 42 | if (helper.merge(result)) { 43 | return helper.succeed(Correctness.CERTAIN); 44 | } else { 45 | return helper.failWithData(Correctness.CERTAIN); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/parsers/minecraft/nbt/tag/lists.ts: -------------------------------------------------------------------------------- 1 | import { CommandErrorBuilder } from "../../../../brigadier/errors"; 2 | import { StringReader } from "../../../../brigadier/string-reader"; 3 | import { ReturnHelper } from "../../../../misc-functions"; 4 | import { LineRange, ReturnedInfo, ReturnSuccess } from "../../../../types"; 5 | import { parseAnyNBTTag } from "../tag-parser"; 6 | import { NodeInfo } from "../util/doc-walker-util"; 7 | import { 8 | getHoverText, 9 | getNBTSuggestions, 10 | LIST_END, 11 | LIST_VALUE_SEP 12 | } from "../util/nbt-util"; 13 | import { NBTWalker } from "../walker"; 14 | 15 | import { NBTTag } from "./nbt-tag"; 16 | 17 | const NOVAL = new CommandErrorBuilder( 18 | "argument.nbt.list.noval", 19 | "Expected ']'" 20 | ); 21 | 22 | export abstract class BaseList extends NBTTag { 23 | protected end: LineRange | undefined; 24 | protected abstract start: LineRange; 25 | protected unclosed: number | undefined; 26 | protected values: NBTTag[] = []; 27 | 28 | public getValue(): NBTTag[] { 29 | return this.values; 30 | } 31 | 32 | public parseInner(reader: StringReader): ReturnedInfo { 33 | const helper = new ReturnHelper(); 34 | if (helper.merge(reader.expect(LIST_END), { errors: false })) { 35 | return helper.succeed(); 36 | } 37 | let index = 0; 38 | while (true) { 39 | this.unclosed = reader.cursor; 40 | const start = reader.cursor; 41 | reader.skipWhitespace(); 42 | if (!reader.canRead()) { 43 | helper.addErrors(NOVAL.create(start, reader.cursor)); 44 | return helper.fail(); 45 | } 46 | const value = parseAnyNBTTag(reader, [ 47 | ...this.path, 48 | `[${index++}]` 49 | ]); 50 | if (helper.merge(value)) { 51 | this.values.push(value.data.tag); 52 | } else { 53 | if (value.data) { 54 | this.values.push(value.data.tag); 55 | this.unclosed = undefined; 56 | } 57 | return helper.fail(); 58 | } 59 | this.unclosed = undefined; 60 | const preEnd = reader.cursor; 61 | reader.skipWhitespace(); 62 | if ( 63 | helper.merge(reader.expect(LIST_VALUE_SEP), { errors: false }) 64 | ) { 65 | continue; 66 | } 67 | if (helper.merge(reader.expect(LIST_END), { errors: false })) { 68 | this.end = { start: preEnd, end: reader.cursor }; 69 | return helper.succeed(); 70 | } 71 | return helper.fail(NOVAL.create(preEnd, reader.cursor)); 72 | } 73 | } 74 | 75 | public setValue(val: NBTTag[]): this { 76 | this.values = val; 77 | return this; 78 | } 79 | 80 | public validateWith( 81 | node: NodeInfo, 82 | children: NodeInfo, 83 | walker: NBTWalker 84 | ): ReturnSuccess { 85 | const helper = new ReturnHelper(); 86 | helper.addActions({ 87 | data: getHoverText(node.node), 88 | high: this.start.end, 89 | low: this.start.start, 90 | type: "hover" 91 | }); 92 | for (const value of this.values) { 93 | helper.merge(value.validate(children, walker)); 94 | } 95 | if (typeof this.unclosed === "number") { 96 | helper.merge(getNBTSuggestions(children.node, this.unclosed)); 97 | } 98 | if (this.end) { 99 | helper.addActions({ 100 | data: getHoverText(node.node), 101 | high: this.start.end, 102 | low: this.start.start, 103 | type: "hover" 104 | }); 105 | } 106 | return helper.succeed(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/parsers/minecraft/nbt/tag/nbt-tag.ts: -------------------------------------------------------------------------------- 1 | import { NBTNode, NoPropertyNode } from "mc-nbt-paths"; 2 | 3 | import { StringReader } from "../../../../brigadier/string-reader"; 4 | import { ReturnHelper } from "../../../../misc-functions"; 5 | import { emptyRange } from "../../../../test/blanks"; 6 | import { 7 | CE, 8 | LineRange, 9 | ReturnedInfo, 10 | ReturnSuccess, 11 | SubAction 12 | } from "../../../../types"; 13 | import { 14 | isTypedInfo, 15 | NodeInfo, 16 | VALIDATION_ERRORS 17 | } from "../util/doc-walker-util"; 18 | import { Correctness, getHoverText } from "../util/nbt-util"; 19 | import { NBTWalker } from "../walker"; 20 | 21 | export type ParseReturn = ReturnedInfo; 22 | export type TagType = 23 | | "byte" 24 | | "short" 25 | | "int" 26 | | "long" 27 | | "float" 28 | | "double" 29 | | "byte_array" 30 | | "int_array" 31 | | "long_array" 32 | | "string" 33 | | "list" 34 | | "compound"; 35 | 36 | export abstract class NBTTag { 37 | protected path: string[]; 38 | protected range: LineRange = emptyRange(); 39 | protected abstract tagType?: TagType; 40 | 41 | public constructor(path: string[]) { 42 | this.path = path; 43 | } 44 | public getRange(): LineRange { 45 | return this.range; 46 | } 47 | 48 | public abstract getValue(): any; 49 | 50 | public parse(reader: StringReader): ParseReturn { 51 | this.range.start = reader.cursor; 52 | const out = this.readTag(reader); 53 | this.range.end = reader.cursor; 54 | // tslint:disable:helper-return 55 | return out; 56 | } 57 | 58 | public abstract setValue(val: any): this; 59 | 60 | public validate( 61 | node: NodeInfo, 62 | // tslint:disable-next-line:variable-name 63 | _walker: NBTWalker 64 | ): ReturnSuccess { 65 | const helper = new ReturnHelper(); 66 | const result = this.sameType(node); 67 | if (!helper.merge(result)) { 68 | return helper.succeed(); 69 | } 70 | helper.addActions(this.rangeHover(node.node)); 71 | return helper.succeed(); 72 | } 73 | 74 | protected rangeHover( 75 | node: NBTNode, 76 | range: LineRange = this.range 77 | ): SubAction { 78 | return { 79 | data: getHoverText(node), 80 | high: range.end, 81 | low: range.start, 82 | type: "hover" 83 | }; 84 | } 85 | 86 | protected abstract readTag(reader: StringReader): ParseReturn; 87 | 88 | protected sameType( 89 | node: NodeInfo, 90 | type: string = this.tagType || "" 91 | ): ReturnedInfo { 92 | const helper = new ReturnHelper(); 93 | if (!isTypedInfo(node) || (node.node as NoPropertyNode).type !== type) { 94 | return helper.fail( 95 | VALIDATION_ERRORS.wrongType.create( 96 | this.range.start, 97 | this.range.end, 98 | (node.node as NoPropertyNode).type || "", 99 | type 100 | ) 101 | ); 102 | } 103 | return helper.succeed(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/parsers/minecraft/nbt/tag/string-tag.ts: -------------------------------------------------------------------------------- 1 | import { StringReader } from "../../../../brigadier/string-reader"; 2 | import { ReturnHelper } from "../../../../misc-functions"; 3 | import { Correctness } from "../util/nbt-util"; 4 | 5 | import { NBTTag, ParseReturn } from "./nbt-tag"; 6 | 7 | export class NBTTagString extends NBTTag { 8 | protected tagType: "string" = "string"; 9 | protected value = ""; 10 | 11 | public getValue(): string { 12 | return this.value; 13 | } 14 | 15 | public setValue(val: string): this { 16 | this.value = val; 17 | return this; 18 | } 19 | 20 | protected readTag(reader: StringReader): ParseReturn { 21 | const helper = new ReturnHelper(); 22 | const quoted = StringReader.isQuotedStringStart(reader.peek()); 23 | const str = reader.readString(); 24 | if (helper.merge(str)) { 25 | this.value = str.data; 26 | if (quoted) { 27 | return helper.succeed(Correctness.CERTAIN); 28 | } 29 | if (str.data.length === 0) { 30 | // E.g. `{`, clearly it is not an unquoted string 31 | return helper.failWithData(Correctness.NO); 32 | } 33 | return helper.succeed(Correctness.MAYBE); 34 | } else { 35 | if (quoted) { 36 | return helper.failWithData(Correctness.CERTAIN); 37 | } 38 | return helper.failWithData(Correctness.NO); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/parsers/minecraft/nbt/tag/typed-list-tag.ts: -------------------------------------------------------------------------------- 1 | import { NoPropertyNode } from "mc-nbt-paths"; 2 | 3 | import { StringReader } from "../../../../brigadier/string-reader"; 4 | import { ReturnHelper } from "../../../../misc-functions"; 5 | import { emptyRange } from "../../../../test/blanks"; 6 | import { LineRange, ReturnSuccess } from "../../../../types"; 7 | import { NodeInfo } from "../util/doc-walker-util"; 8 | import { Correctness } from "../util/nbt-util"; 9 | import { NBTWalker } from "../walker"; 10 | 11 | import { BaseList } from "./lists"; 12 | import { ParseReturn } from "./nbt-tag"; 13 | 14 | type ArrayType = "byte_array" | "int_array" | "long_array"; 15 | const types: Array<["B" | "I" | "L", "byte" | "int" | "long", ArrayType]> = [ 16 | ["B", "byte", "byte_array"], 17 | ["I", "int", "int_array"], 18 | ["L", "long", "long_array"] 19 | ]; 20 | 21 | export class NBTTagTypedList extends BaseList { 22 | protected start: LineRange = emptyRange(); 23 | protected tagType: ArrayType | undefined = undefined; 24 | /** Only used for when we start incorrectly */ 25 | private remaining: string | undefined; 26 | private startIndex = -1; 27 | 28 | public validate( 29 | anyInfo: NodeInfo, 30 | walker: NBTWalker 31 | ): ReturnSuccess { 32 | const helper = new ReturnHelper(); 33 | const result = this.sameType(anyInfo); 34 | if (!helper.merge(result)) { 35 | return helper.succeed(); 36 | } 37 | const info = anyInfo as NodeInfo; 38 | const type = types.find( 39 | v => v["2"] === this.tagType /* === info.type */ 40 | ); 41 | if (type) { 42 | helper.merge( 43 | this.validateWith( 44 | info, 45 | { node: { type: type["1"] }, path: info.path }, 46 | walker 47 | ) 48 | ); 49 | const toCheck = `[${type["0"]};`; 50 | if (this.remaining) { 51 | if (toCheck.startsWith(this.remaining)) { 52 | helper.addSuggestion(this.startIndex, toCheck); 53 | } 54 | } 55 | } 56 | return helper.succeed(); 57 | } 58 | 59 | protected readTag(reader: StringReader): ParseReturn { 60 | const start = reader.cursor; 61 | this.startIndex = start; 62 | const helper = new ReturnHelper(); 63 | const remaining = reader.getRemaining(); 64 | const result = remaining.match(/^\[([BIL]);/); 65 | if (result) { 66 | reader.skipWhitespace(); 67 | this.start = { start, end: reader.cursor }; 68 | const type = types.find(v => v[0] === result[1]); 69 | if (type) { 70 | this.tagType = type[2]; 71 | } else { 72 | // `unreachable!` 73 | } 74 | const innerResult = this.parseInner(reader); 75 | if (helper.merge(innerResult)) { 76 | return helper.succeed(Correctness.CERTAIN); 77 | } else { 78 | return helper.failWithData(Correctness.CERTAIN); 79 | } 80 | } else { 81 | this.remaining = remaining; 82 | return helper.failWithData(Correctness.NO); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/parsers/minecraft/nbt/util/array-reader.ts: -------------------------------------------------------------------------------- 1 | export class ArrayReader { 2 | private index = 0; 3 | private readonly inner: string[]; 4 | 5 | public constructor(arr: string[]) { 6 | this.inner = arr; 7 | } 8 | 9 | public canRead(length: number = 1): boolean { 10 | return this.index + length <= this.inner.length; 11 | } 12 | 13 | public getArray(): string[] { 14 | return this.inner; 15 | } 16 | 17 | public getIndex(): number { 18 | return this.index; 19 | } 20 | 21 | public getRead(): string[] { 22 | return this.inner.slice(0, this.index); 23 | } 24 | 25 | public insert(vals: string[], index: number = 0): void { 26 | this.inner.splice(index, 0, ...vals); 27 | } 28 | 29 | public peek(): string { 30 | return this.inner[this.index]; 31 | } 32 | 33 | public read(): string { 34 | return this.inner[this.index++]; 35 | } 36 | 37 | public setIndex(val: number): void { 38 | this.index = val; 39 | } 40 | 41 | public skip(): void { 42 | this.index++; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/parsers/minecraft/nbt/util/doc-walker-util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompoundNode, 3 | FunctionNode, 4 | ListNode, 5 | NBTNode, 6 | NoPropertyNode, 7 | RefNode, 8 | RootNode 9 | } from "mc-nbt-paths"; 10 | import * as path from "path"; 11 | import * as url from "url"; 12 | import { DiagnosticSeverity } from "vscode-languageserver/lib/main"; 13 | 14 | import { CommandErrorBuilder } from "../../../../brigadier/errors"; 15 | import { NBTTagCompound } from "../tag/compound-tag"; 16 | import { BaseList } from "../tag/lists"; 17 | import { NBTTag } from "../tag/nbt-tag"; 18 | 19 | export const parseRefPath = ( 20 | ref: string, 21 | currentPath: string 22 | ): [string, string[]] => { 23 | const cpd = path.dirname(currentPath); 24 | const refurl = url.parse(ref); 25 | const fragPath = (refurl.hash || "#") 26 | .substring(1) 27 | .split("/") 28 | .filter(v => v !== ""); 29 | const nextPath = path.posix.join( 30 | cpd, 31 | refurl.path || path.basename(currentPath) 32 | ); 33 | return [nextPath, fragPath]; 34 | }; 35 | 36 | export function getNBTTagFromTree( 37 | tag: NBTTag, 38 | nbtPath: string[] 39 | ): NBTTag | undefined { 40 | let lastTag: NBTTag | undefined = tag; 41 | for (const s of nbtPath) { 42 | // tslint:disable:no-require-imports This fixes a circular dependency issue 43 | if ( 44 | lastTag instanceof require("../tag/lists").BaseList && 45 | /\d+/.test(s) 46 | ) { 47 | lastTag = (lastTag as BaseList).getValue()[parseInt(s, 10)]; 48 | } else if (lastTag instanceof require("../tag/compound-tag").BaseList) { 49 | lastTag = (lastTag as NBTTagCompound).getValue().get(s); 50 | } else { 51 | return undefined; 52 | } 53 | // tslint:enable:no-require-imports 54 | } 55 | return lastTag; 56 | } 57 | 58 | /** 59 | * type parameter N is used to allow passing nodes whose type have already been determined 60 | */ 61 | export interface NodeInfo { 62 | readonly node: N; 63 | readonly path: string; 64 | } 65 | 66 | export function isRefNode(node: NBTNode): node is RefNode { 67 | return node.hasOwnProperty("ref"); 68 | } 69 | 70 | export function isFunctionNode(node: NBTNode): node is FunctionNode { 71 | return node.hasOwnProperty("function"); 72 | } 73 | 74 | export type TypedNode = NoPropertyNode | CompoundNode | ListNode | RootNode; 75 | export function isTypedNode(node: NBTNode): node is TypedNode { 76 | return node.hasOwnProperty("type"); 77 | } 78 | 79 | export function isCompoundNode(node: NBTNode): node is CompoundNode { 80 | return isTypedNode(node) && node.type === "compound"; 81 | } 82 | 83 | export function isRootNode(node: NBTNode): node is RootNode { 84 | return isTypedNode(node) && node.type === "root"; 85 | } 86 | 87 | export function isListNode(node: NBTNode): node is ListNode { 88 | return isTypedNode(node) && node.type === "list"; 89 | } 90 | 91 | // Return type is a lie to allow using the convert function below 92 | export function isNoNBTNode(node: NBTNode): node is NoPropertyNode { 93 | return isTypedNode(node) && node.type === "no-nbt"; 94 | } 95 | 96 | export const isRefInfo = convert(isRefNode); 97 | export const isFunctionInfo = convert(isFunctionNode); 98 | export const isTypedInfo = convert(isTypedNode); 99 | export const isCompoundInfo = convert(isCompoundNode); 100 | export const isRootInfo = convert(isRootNode); 101 | export const isListInfo = convert(isListNode); 102 | export const isNoNBTInfo = convert(isNoNBTNode); 103 | 104 | function convert( 105 | f: (node: NBTNode) => node is T 106 | ): (info: NodeInfo) => info is NodeInfo { 107 | return (info): info is NodeInfo => f(info.node); 108 | } 109 | 110 | export interface NBTValidationInfo { 111 | endPos: number; 112 | extraChildren: boolean; 113 | compoundMerge(): CompoundNode; // This is so the compound parser can merge child ref on call 114 | } 115 | 116 | export const VALIDATION_ERRORS = { 117 | badIndex: new CommandErrorBuilder( 118 | "argument.nbt.validation.list.badpath", 119 | "The index '%s' is not a valid index" 120 | ), 121 | noSuchChild: new CommandErrorBuilder( 122 | "argument.nbt.validation.compound.nochild", 123 | "The tag does not have a child named '%s'", 124 | DiagnosticSeverity.Warning 125 | ), 126 | wrongType: new CommandErrorBuilder( 127 | "argument.nbt.validation.wrongtype", 128 | "Expected nbt value to be %s, got %s" 129 | ) 130 | }; 131 | -------------------------------------------------------------------------------- /src/parsers/minecraft/swizzle.ts: -------------------------------------------------------------------------------- 1 | import { power } from "js-combinatorics"; 2 | 3 | import { CommandErrorBuilder } from "../../brigadier/errors"; 4 | import { StringReader } from "../../brigadier/string-reader"; 5 | import { ReturnHelper } from "../../misc-functions"; 6 | import { Parser } from "../../types"; 7 | 8 | const values = ["x", "y", "z"]; 9 | 10 | const INVALID_CHAR = new CommandErrorBuilder( 11 | "argument.swizzle.invalid", 12 | "Invalid character '%s'" 13 | ); 14 | const DUPLICATE = new CommandErrorBuilder( 15 | "argument.swizzle.duplicate", 16 | "Duplicate character '%s'" 17 | ); 18 | 19 | export const swizzleParer: Parser = { 20 | parse: (reader: StringReader) => { 21 | const helper = new ReturnHelper(); 22 | const start = reader.cursor; 23 | const arr = reader.readUnquotedString().split(""); 24 | for (const s of arr) { 25 | if (values.indexOf(s) === -1) { 26 | return helper.fail( 27 | INVALID_CHAR.create(start, reader.cursor, s) 28 | ); 29 | } 30 | } 31 | for (const v of values) { 32 | if (arr.indexOf(v) !== arr.lastIndexOf(v)) { 33 | return helper.fail(DUPLICATE.create(start, reader.cursor, v)); 34 | } 35 | } 36 | const text = reader.string.substring(start, reader.cursor); 37 | const suggestions = power(values.filter(v => text.indexOf(v) === -1)) 38 | .map(v => text + v.join("")) 39 | .filter(v => v !== ""); 40 | suggestions.forEach(v => helper.addSuggestion(0, v)); 41 | return helper.succeed(); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/parsers/minecraft/time.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver"; 2 | 3 | import { CommandErrorBuilder } from "../../brigadier/errors"; 4 | import { StringReader } from "../../brigadier/string-reader"; 5 | import { ReturnHelper } from "../../misc-functions"; 6 | import { typed_keys } from "../../misc-functions/third_party/typed-keys"; 7 | import { Parser } from "../../types"; 8 | 9 | const EXCEPTIONS = { 10 | invalid_unit: new CommandErrorBuilder( 11 | "argument.time.invalid_unit", 12 | "Invalid unit '%s'" 13 | ), 14 | // This is not tested 15 | not_integer: new CommandErrorBuilder( 16 | "argument.time.not_nonnegative_integer", 17 | "Not a non-negative integer number of ticks: %s (Due to differences in the representation of floating point numbers, Minecraft Java Edition might not fail for this value)", 18 | DiagnosticSeverity.Warning 19 | ), 20 | not_nonegative_integer: new CommandErrorBuilder( 21 | "argument.time.not_nonnegative_integer", 22 | "Not a non-negative integer number of ticks: %s" 23 | ) 24 | }; 25 | 26 | export const times = { 27 | d: 24000, 28 | s: 20, 29 | t: 1 30 | }; 31 | 32 | export const timeParser: Parser = { 33 | parse: (reader, info) => { 34 | const helper = new ReturnHelper(info); 35 | const start = reader.cursor; 36 | const float = reader.readFloat(); 37 | if (!helper.merge(float)) { 38 | return helper.fail(); 39 | } 40 | const suffixStart = reader.cursor; 41 | const suffix = reader.readOption(typed_keys(times), { 42 | quote: false, 43 | unquoted: StringReader.charAllowedInUnquotedString 44 | }); 45 | if (!helper.merge(suffix)) { 46 | if (suffix.data !== "") { 47 | return helper 48 | .addErrors( 49 | EXCEPTIONS.invalid_unit.create( 50 | suffixStart, 51 | reader.cursor, 52 | suffix.data || "none" 53 | ) 54 | ) 55 | .succeed(); 56 | } 57 | } 58 | const unit = times[suffix.data as keyof typeof times] || 1; 59 | const result = unit * float.data; 60 | if (result < 0) { 61 | return helper 62 | .addErrors( 63 | EXCEPTIONS.not_nonegative_integer.create( 64 | start, 65 | reader.cursor, 66 | result.toString() 67 | ) 68 | ) 69 | .succeed(); 70 | } 71 | if (!Number.isInteger(result)) { 72 | return helper 73 | .addErrors( 74 | EXCEPTIONS.not_integer.create( 75 | start, 76 | reader.cursor, 77 | result.toString() 78 | ) 79 | ) 80 | .succeed(); 81 | } 82 | return helper.succeed(); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /src/test/actions.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { SignatureInformation } from "vscode-languageserver"; 3 | 4 | import { signatureHelpProvider } from "../actions"; 5 | import { DataManager } from "../data/manager"; 6 | import { CommandTree } from "../data/types"; 7 | 8 | import { assertMembers, unwrap } from "./assertions"; 9 | import { emptyGlobal } from "./blanks"; 10 | 11 | const tree: CommandTree = { 12 | children: { 13 | redirect: { type: "literal", redirect: ["segment1", "segment2"] }, 14 | run: { type: "literal" }, 15 | segment1: { 16 | children: { segment2: { type: "literal", executable: true } }, 17 | type: "literal" 18 | }, 19 | simple: { type: "literal", executable: true } 20 | }, 21 | type: "root" 22 | }; 23 | const manager = DataManager.newWithData({ 24 | ...emptyGlobal, 25 | commands: tree 26 | }); 27 | 28 | describe("signatureHelpProvider()", () => { 29 | it("should give results for every command when no text is specified", () => { 30 | const result = unwrap( 31 | signatureHelpProvider( 32 | { 33 | parseInfo: { nodes: [], actions: [], errors: [] }, 34 | text: "" 35 | }, 36 | { line: 0, character: 0 }, 37 | {} as any, 38 | manager 39 | ) 40 | ); 41 | const expected = { 42 | activeParameter: 0, 43 | activeSignature: 0 44 | }; 45 | assertMembers( 46 | result.signatures, 47 | [ 48 | "redirect", 49 | "run redirect|run|segment1|simple", // Root redirect 50 | "segment1 segment2", 51 | "simple" 52 | ], 53 | (a, e) => a.label === e 54 | ); 55 | delete result.signatures; 56 | assert.deepStrictEqual(result, expected); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/test/blanks.ts: -------------------------------------------------------------------------------- 1 | import { Range } from "vscode-languageserver"; 2 | 3 | import { GlobalData } from "../data/types"; 4 | import { PackLocationSegments } from "../misc-functions"; 5 | import { CommandData, LineRange } from "../types"; 6 | 7 | import { ReturnAssertionInfo, TestParserInfo } from "./assertions"; 8 | 9 | /** 10 | * Blank items for testing 11 | */ 12 | 13 | // tslint:disable-next-line:variable-name This allows for the property declaration shorthand 14 | export const pack_segments: PackLocationSegments = { 15 | pack: "", 16 | packsFolder: "", 17 | rest: "" 18 | }; 19 | 20 | export const succeeds: ReturnAssertionInfo = { succeeds: true }; 21 | 22 | export const emptyRange = (): LineRange => ({ start: 0, end: 0 }); 23 | export const blankproperties: TestParserInfo = { 24 | context: {}, 25 | data: {} as CommandData, 26 | node_properties: {}, 27 | path: ["test"] 28 | }; 29 | 30 | const set = new Set(); 31 | export const emptyGlobal: GlobalData = { 32 | blocks: {}, 33 | commands: { type: "root" }, 34 | jsonService: undefined as any, 35 | meta_info: { version: "" }, 36 | nbt_docs: new Map(), 37 | registries: { 38 | // tslint:disable:object-literal-sort-keys - very little value 39 | "minecraft:block": set, 40 | "minecraft:fluid": set, 41 | "minecraft:sound_event": set, 42 | "minecraft:mob_effect": set, 43 | "minecraft:enchantment": set, 44 | "minecraft:entity_type": set, 45 | "minecraft:item": set, 46 | "minecraft:potion": set, 47 | "minecraft:carver": set, 48 | "minecraft:surface_builder": set, 49 | "minecraft:feature": set, 50 | "minecraft:decorator": set, 51 | "minecraft:biome": set, 52 | "minecraft:particle_type": set, 53 | "minecraft:biome_source_type": set, 54 | "minecraft:block_entity_type": set, 55 | "minecraft:chunk_generator_type": set, 56 | "minecraft:dimension_type": set, 57 | "minecraft:motive": set, 58 | "minecraft:custom_stat": set, 59 | "minecraft:chunk_status": set, 60 | "minecraft:structure_feature": set, 61 | "minecraft:structure_piece": set, 62 | "minecraft:rule_test": set, 63 | "minecraft:structure_processor": set, 64 | "minecraft:structure_pool_element": set, 65 | "minecraft:menu": set, 66 | "minecraft:recipe_type": set, 67 | "minecraft:recipe_serializer": set, 68 | "minecraft:stat_type": set, 69 | "minecraft:villager_type": set, 70 | "minecraft:villager_profession": set 71 | // tslint:enable:object-literal-sort-keys 72 | }, 73 | resources: {} 74 | }; 75 | 76 | export const blankRange: Range = { 77 | end: { line: 0, character: 0 }, 78 | start: { line: 0, character: 0 } 79 | }; 80 | -------------------------------------------------------------------------------- /src/test/data/NOTICE.txt: -------------------------------------------------------------------------------- 1 | The Data Folder is the only folder which should contains references to accessing the file system. 2 | Because of this, the `resources` folder contains example data for use with testing -------------------------------------------------------------------------------- /src/test/data/cache.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | // tslint:disable-next-line:no-implicit-dependencies This is only for testing 5 | import * as rimraf from "rimraf"; 6 | import { promisify } from "util"; 7 | 8 | import * as cache from "../../data/cache"; 9 | import { Cacheable } from "../../data/types"; 10 | 11 | const rimrafAsync = promisify(rimraf); 12 | 13 | const cacheFolder = path.join(process.cwd(), "cache"); 14 | const testData: Cacheable = { 15 | blocks: { 16 | "minecraft:chocolate": {} 17 | }, 18 | commands: { 19 | children: { 20 | hello: { 21 | type: "literal" 22 | } 23 | }, 24 | type: "root" 25 | }, 26 | meta_info: { version: "1.13" }, 27 | registries: { "minecraft:items": new Set(["test", "test2"]) }, 28 | resources: {} 29 | } as any; 30 | describe("Cache Management", () => { 31 | after(async () => rimrafAsync(cacheFolder)); 32 | describe("cacheData", () => { 33 | it("should cache the data in the folder", async () => { 34 | await cache.cacheData(testData); 35 | const files = await promisify(fs.readdir)(cacheFolder); 36 | assert.deepStrictEqual( 37 | files.sort(), 38 | [ 39 | "commands.json", 40 | "blocks.json", 41 | "meta_info.json", 42 | "registries.json", 43 | "resources.json" 44 | ].sort() 45 | ); 46 | }); 47 | }); 48 | describe("readCache", () => { 49 | it("should be consistent with cacheData", async () => { 50 | await cache.cacheData(testData); 51 | const data = await cache.readCache(); 52 | assert.deepStrictEqual(data, testData); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/test/data/datapack-resource.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as path from "path"; 3 | 4 | import { getPacksInfo } from "../../data/datapack-resources"; 5 | import { Datapack, MinecraftResource, WorldInfo } from "../../data/types"; 6 | import { typed_keys } from "../../misc-functions/third_party/typed-keys"; 7 | import { 8 | assertMembers, 9 | assertNamespaces, 10 | convertToResource, 11 | returnAssert 12 | } from "../assertions"; 13 | import { emptyGlobal } from "../blanks"; 14 | 15 | // Tests are run from within the lib folder, but data is in the root 16 | const rootFolder = path.join(process.cwd(), "test_data"); 17 | describe("Datapack Resource Testing", () => { 18 | it("should return the correct data", async () => { 19 | const datapacks = path.join(rootFolder, "test_world", "datapacks"); 20 | const expected: { [pack: string]: Datapack } = { 21 | ExampleDatapack: { 22 | data: { 23 | function_tags: [convertToResource("minecraft:tick")], 24 | functions: [convertToResource("test_namespace:function")] 25 | }, 26 | id: 0, 27 | mcmeta: { 28 | pack: { pack_format: 3, description: "test datapack" } 29 | }, 30 | name: "ExampleDatapack" 31 | }, 32 | ExampleDatapack2: { 33 | data: { 34 | functions: [convertToResource("test_namespace:function")] 35 | }, 36 | id: 1, 37 | mcmeta: { 38 | pack: { pack_format: 3, description: "test datapack 2" } 39 | }, 40 | name: "ExampleDatapack2" 41 | } 42 | }; 43 | const result = await getPacksInfo( 44 | path.join(rootFolder, "test_world", "datapacks"), 45 | emptyGlobal 46 | ); 47 | returnAssert(result, { succeeds: true, numMisc: 5 }); 48 | assert(result.misc.every(v => v.kind === "ClearError")); 49 | assertPacksInfo(result.data, datapacks, expected); 50 | }); 51 | }); 52 | 53 | function assertPacksInfo( 54 | result: WorldInfo, 55 | location: string, 56 | packs: { [key: string]: Datapack } 57 | ): void { 58 | assert.strictEqual(result.location, location); 59 | for (const packName of Object.keys(packs)) { 60 | if (!result.packnamesmap.hasOwnProperty(packName)) { 61 | throw new assert.AssertionError({ 62 | message: `Expected pack with name of ${packName}` 63 | }); 64 | } 65 | const pack = packs[packName]; 66 | const id = result.packnamesmap[packName]; 67 | const actual = result.packs[id]; 68 | assert.strictEqual(actual.id, id); 69 | const keys = typed_keys(pack.data); 70 | assertMembers(keys, Object.keys(actual.data), (a, b) => a === b); 71 | 72 | for (const kind of keys) { 73 | const resources = pack.data[kind] as MinecraftResource[]; 74 | const actualResources = actual.data[kind] as MinecraftResource[]; 75 | assert(actualResources.every(v => v.pack === id)); 76 | assertNamespaces(resources, actualResources); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/data/nbt.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as Long from "long"; 3 | 4 | import { loadNBT } from "../../data/nbt/nbt-cache"; 5 | import { LevelData } from "../../data/nbt/nbt-types"; 6 | import { parse } from "../../data/nbt/parser"; 7 | import { readFileAsync } from "../../misc-functions"; 8 | 9 | interface BigTest { 10 | // This name is from the test file provided by Notch 11 | "byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))": number[]; 12 | byteTest: number; 13 | doubleTest: number; 14 | floatTest: number; 15 | intTest: number; 16 | "listTest (compound)": Array<{ 17 | "created-on": Long; 18 | name: string; 19 | }>; 20 | "listTest (long)": Long[]; 21 | longTest: Long; 22 | "nested compound test": { 23 | egg: { 24 | name: string; 25 | value: number; 26 | }; 27 | ham: { 28 | name: string; 29 | value: number; 30 | }; 31 | }; 32 | shortTest: number; 33 | stringTest: string; 34 | } 35 | 36 | const bigtest: BigTest = { 37 | // *thanks notch* 38 | "byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))": new Array< 39 | number 40 | >(1000) 41 | .fill(0) 42 | .map((_, n) => (n * n * 255 + n * 7) % 100), 43 | byteTest: 127, 44 | doubleTest: 0.493128713218231, 45 | floatTest: 0.4982315, 46 | intTest: 2147483647, 47 | "listTest (compound)": [ 48 | { 49 | "created-on": Long.fromString("1264099775885"), 50 | name: "Compound tag #0" 51 | }, 52 | { 53 | "created-on": Long.fromString("1264099775885"), 54 | name: "Compound tag #1" 55 | } 56 | ], 57 | "listTest (long)": [11, 12, 13, 14, 15].map(v => Long.fromNumber(v)), 58 | longTest: Long.fromString("9223372036854775807"), 59 | "nested compound test": { 60 | egg: { 61 | name: "Eggbert", 62 | value: 0.5 63 | }, 64 | ham: { 65 | name: "Hampus", 66 | value: 0.75 67 | } 68 | }, 69 | shortTest: 32767, 70 | stringTest: "HELLO WORLD THIS IS A TEST STRING ÅÄÖ!" 71 | }; 72 | 73 | describe("(binary) nbt parser tests", () => { 74 | it("should parse bigtest.nbt", async () => { 75 | const data = await readFileAsync("test_data/test_nbt/bigtest.nbt"); 76 | const nbt: BigTest = await parse(data); 77 | for (const rkey of Object.keys(nbt)) { 78 | const key = rkey as keyof BigTest; 79 | const val = nbt[key]; 80 | const altval = bigtest[key]; 81 | if ( 82 | typeof val === "number" && 83 | typeof altval === "number" && 84 | !Number.isInteger(val) 85 | ) { 86 | assert.strictEqual(altval.toPrecision(7), val.toPrecision(7)); 87 | } else { 88 | assert.deepStrictEqual(val, altval); 89 | } 90 | } 91 | }); 92 | 93 | it("should parse level.dat", async () => { 94 | const nbt = await loadNBT("test_data/test_world"); 95 | if (!!nbt.level) { 96 | const data = nbt.level.Data; 97 | assert.strictEqual(data.version, 19133); 98 | assert.deepStrictEqual(data.Version, { 99 | Id: 1628, 100 | Name: "1.13.1", 101 | Snapshot: 0 102 | } as LevelData["Version"]); 103 | assert.deepStrictEqual(data.CustomBossEvents, { 104 | bossbar: { 105 | Color: "red", 106 | CreateWorldFog: 0, 107 | DarkenScreen: 0, 108 | Max: 20, 109 | Name: `{"color":"white","text":"Custom Bar"}`, 110 | Overlay: "notched_20", 111 | PlayBossMusic: 0, 112 | Players: [ 113 | { 114 | L: Long.fromString("-7482673864369647603"), 115 | M: Long.fromString("-694532656901238868") 116 | } 117 | ], 118 | Value: 0, 119 | Visible: 1 120 | } 121 | } as LevelData["CustomBossEvents"]); 122 | } else { 123 | assert.fail("level.dat not loaded"); 124 | } 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/test/logging-setup.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | const logger = (message: string) => { 4 | // tslint:disable-next-line:no-console 5 | console.log(message); 6 | }; 7 | 8 | process.env.MCFUNCTION_CACHE_DIR = path.join(process.cwd(), "cache"); 9 | 10 | // tslint:disable-next-line:prefer-object-spread 11 | global.mcLangLog = Object.assign(logger, { 12 | internal: (message: string) => { 13 | logger(`[McFunctionInternal] ${message}`); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/misc-functions/context.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { 4 | ContextPath, 5 | resolvePaths, 6 | startPaths 7 | } from "../../misc-functions/context"; 8 | 9 | describe("context tests", () => { 10 | type pth = ContextPath; 11 | const options: pth[] = [ 12 | { data: "yay", path: ["foo1", "foo2", "foo3"] }, 13 | { data: "longer", path: ["1", "2", "3", "4"] }, 14 | { data: "shorter", path: ["1"] } 15 | ]; 16 | describe("resolvePaths()", () => { 17 | it("should return the correct paths", () => { 18 | assert.strictEqual( 19 | resolvePaths(options, ["foo1", "foo2", "foo3"]), 20 | "yay" 21 | ); 22 | }); 23 | it("should return undefined if it cannot find the path", () => { 24 | assert.strictEqual( 25 | resolvePaths(options, ["blip", "blop"]), 26 | undefined 27 | ); 28 | }); 29 | }); 30 | describe("startPaths", () => { 31 | it("should return the correct value when an exact match is found", () => { 32 | assert.strictEqual( 33 | startPaths(options, ["foo1", "foo2", "foo3"]), 34 | "yay" 35 | ); 36 | }); 37 | it("should return undefined if it cannot find the path", () => { 38 | assert.strictEqual( 39 | startPaths(options, ["blip", "blop"]), 40 | undefined 41 | ); 42 | }); 43 | it("should allow an extended path", () => { 44 | assert.strictEqual( 45 | startPaths(options, ["foo1", "foo2", "foo3", "foo4"]), 46 | "yay" 47 | ); 48 | }); 49 | it("should give a longer path than a shorter path", () => { 50 | assert.strictEqual( 51 | startPaths(options, ["1", "2", "3", "4"]), 52 | "longer" 53 | ); 54 | }); 55 | it("should give a shorter path for part of a longer", () => { 56 | assert.strictEqual(startPaths(options, ["1", "2", "3"]), "shorter"); 57 | }); 58 | it("should give a longer path when there are too many characters", () => { 59 | assert.strictEqual( 60 | startPaths(options, ["1", "2", "3", "4", "5"]), 61 | "longer" 62 | ); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/test/misc-functions/creators.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { 4 | createCommandLines, 5 | createParserInfo, 6 | splitLines 7 | } from "../../misc-functions/creators"; 8 | import { CommandData, ParserInfo } from "../../types"; 9 | 10 | describe("Instance Creation Functions (Misc)", () => { 11 | describe("createCommandLines()", () => { 12 | it("should convert an array of strings into an array of Command Lines", () => { 13 | assert.deepStrictEqual( 14 | createCommandLines(["line number 1", "line number two"]), 15 | [{ text: "line number 1" }, { text: "line number two" }] 16 | ); 17 | }); 18 | }); 19 | 20 | const data = {} as CommandData; 21 | describe("createParserInfo", () => { 22 | it("should create a parser info based on a node's properties", () => { 23 | const expected: ParserInfo = { 24 | context: {}, 25 | data, 26 | node_properties: { test: true }, 27 | path: ["testpath"], 28 | suggesting: false 29 | }; 30 | const result = createParserInfo( 31 | { 32 | properties: { test: true }, 33 | type: "literal" 34 | }, 35 | data, 36 | ["testpath"], 37 | {}, 38 | false 39 | ); 40 | assert.deepStrictEqual(result, expected); 41 | }); 42 | it("should create a blank node properties if it doesn't exist", () => { 43 | const expected: ParserInfo = { 44 | context: {}, 45 | data, 46 | node_properties: {}, 47 | path: ["testpath"], 48 | suggesting: false 49 | }; 50 | const result = createParserInfo( 51 | { 52 | type: "literal" 53 | }, 54 | data, 55 | ["testpath"], 56 | {}, 57 | false 58 | ); 59 | assert.deepStrictEqual(result, expected); 60 | }); 61 | }); 62 | 63 | describe("splitLines", () => { 64 | it("should convert a multiple line string", () => { 65 | assert.deepStrictEqual( 66 | splitLines("first line\nsecond line\nthird line"), 67 | [ 68 | { text: "first line" }, 69 | { text: "second line" }, 70 | { text: "third line" } 71 | ] 72 | ); 73 | }); 74 | it("should create a single command line from a single line string", () => { 75 | assert.deepStrictEqual(splitLines("singleline"), [ 76 | { text: "singleline" } 77 | ]); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/test/misc-functions/group-resources.test.ts: -------------------------------------------------------------------------------- 1 | import { getResourcesofType } from "../../misc-functions/group-resources"; 2 | import { CommandData } from "../../types"; 3 | import { assertNamespaces, convertToResource } from "../assertions"; 4 | 5 | const dummyData: CommandData = { 6 | globalData: { 7 | resources: { 8 | advancements: [ 9 | { 10 | namespace: "minecraft", 11 | path: "test" 12 | } 13 | ], 14 | functions: [convertToResource("minecraft:test2")] 15 | } 16 | } as any, 17 | localData: { 18 | current: 0, 19 | location: "", 20 | nbt: {}, 21 | packnamesmap: { testpack: 0, pack2: 1 }, 22 | packs: { 23 | 0: { 24 | data: { 25 | advancements: [ 26 | { 27 | namespace: "local", 28 | pack: 0, 29 | path: "test" 30 | }, 31 | { 32 | namespace: "local", 33 | pack: 0, 34 | path: "testfolder/testchild" 35 | }, 36 | { 37 | namespace: "other", 38 | pack: 0, 39 | path: "secondtest" 40 | } 41 | ] 42 | }, 43 | id: 0, 44 | name: "testpack" 45 | }, 46 | 1: { 47 | data: { 48 | advancements: [ 49 | { namespace: "secondpath", path: "path", pack: 1 } 50 | ] 51 | }, 52 | id: 1, 53 | name: "pack2" 54 | } 55 | } 56 | } 57 | }; 58 | 59 | describe("Group Resources (Misc)", () => { 60 | it("should collect the specified type of resource", () => { 61 | const result = getResourcesofType(dummyData, "advancements"); 62 | assertNamespaces( 63 | [ 64 | convertToResource("minecraft:test"), 65 | convertToResource("local:test"), 66 | convertToResource("local:testfolder/testchild"), 67 | convertToResource("other:secondtest"), 68 | convertToResource("secondpath:path") 69 | ], 70 | result 71 | ); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/test/misc-functions/node-tree.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { CommandNode, CommandTree } from "../../data/types"; 4 | import { followPath, getNextNode } from "../../misc-functions/node-tree"; 5 | 6 | const tree: CommandTree = { 7 | children: { 8 | redirect: { type: "literal", redirect: ["segment1", "segment2"] }, 9 | run: { type: "literal" }, 10 | segment1: { 11 | children: { segment2: { type: "literal", executable: true } }, 12 | type: "literal" 13 | }, 14 | simple: { type: "literal", executable: true } 15 | }, 16 | type: "root" 17 | }; 18 | 19 | describe("Node Tree Manipulation (Misc)", () => { 20 | describe("followPath()", () => { 21 | it("should follow the simple segments", () => { 22 | const result = followPath(tree, ["simple"]); 23 | assert.deepStrictEqual( 24 | result, 25 | // @ts-ignore 26 | tree.children.simple 27 | ); 28 | }); 29 | it("should follow a multi segment path", () => { 30 | const result = followPath(tree, ["segment1", "segment2"]); 31 | assert.deepStrictEqual( 32 | result, 33 | // @ts-ignore 34 | tree.children.segment1.children.segment2 35 | ); 36 | }); 37 | }); 38 | describe("getNextNode()", () => { 39 | it("should give the direct node if there is no redirect", () => { 40 | const node: CommandNode = { type: "literal", executable: true }; 41 | assert.deepStrictEqual(getNextNode(node, ["simple"], tree), { 42 | node, 43 | path: ["simple"] 44 | }); 45 | }); 46 | it("should follow the redirect if there is a redirect", () => { 47 | const node: CommandNode = { type: "literal", executable: true }; 48 | const path = ["redirect"]; 49 | assert.deepStrictEqual( 50 | getNextNode(followPath(tree, path), path, tree), 51 | { 52 | node, 53 | path: ["segment1", "segment2"] 54 | } 55 | ); 56 | }); 57 | it("should redirect to the root if there is a non-executable node with no children and no redirect", () => { 58 | const path = ["run"]; 59 | assert.deepStrictEqual( 60 | getNextNode(followPath(tree, path), path, tree), 61 | { 62 | node: tree, 63 | path: [] 64 | } 65 | ); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/test/misc-functions/parsing/nmsp-tag.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { CommandErrorBuilder } from "../../../brigadier/errors"; 4 | import { StringReader } from "../../../brigadier/string-reader"; 5 | import { convertToNamespace, namespacesEqual } from "../../../misc-functions"; 6 | import { parseNamespaceOrTag } from "../../../misc-functions/parsing/nmsp-tag"; 7 | import { ParserInfo } from "../../../types"; 8 | import { 9 | assertNamespaces, 10 | convertToResource, 11 | returnAssert 12 | } from "../../assertions"; 13 | import { blankproperties, succeeds } from "../../blanks"; 14 | 15 | const data: ParserInfo = { 16 | ...blankproperties, 17 | data: { 18 | globalData: { 19 | resources: { 20 | block_tags: [ 21 | convertToResource("minecraft:tick", { 22 | values: ["#outside:tock", "minecraft:stone"] 23 | }), 24 | convertToResource("outside:tock", { 25 | values: ["minecraft:white_wool"] 26 | }) 27 | ] 28 | } 29 | }, 30 | localData: { 31 | packs: { 32 | 0: { 33 | data: { 34 | block_tags: [ 35 | convertToResource("outside:tock", { 36 | values: ["minecraft:red_wool"] 37 | }) 38 | ] 39 | }, 40 | name: "test1" 41 | } 42 | } 43 | } 44 | } as any, 45 | 46 | suggesting: false 47 | }; 48 | 49 | const errorBuilder = new CommandErrorBuilder("test", "test"); 50 | 51 | describe("parseNamespaceOrTag", () => { 52 | it("should allow a normal namespace to be parsed", () => { 53 | const reader = new StringReader("minecraft:stone"); 54 | const result = parseNamespaceOrTag( 55 | reader, 56 | { ...blankproperties, suggesting: false }, 57 | errorBuilder 58 | ); 59 | if (returnAssert(result, succeeds)) { 60 | assert( 61 | namespacesEqual( 62 | result.data.parsed, 63 | convertToNamespace("minecraft:stone") 64 | ) 65 | ); 66 | } 67 | }); 68 | it("should give an error with an invalid namespace", () => { 69 | const reader = new StringReader("minecraft:fail:surplus"); 70 | const result = parseNamespaceOrTag( 71 | reader, 72 | { ...blankproperties, suggesting: false }, 73 | errorBuilder 74 | ); 75 | if ( 76 | returnAssert(result, { 77 | errors: [ 78 | { 79 | code: "argument.id.invalid", 80 | range: { start: 14, end: 15 } 81 | } 82 | ], 83 | succeeds: false 84 | }) 85 | ) { 86 | assert.strictEqual(result.data, undefined); 87 | } 88 | }); 89 | it("should give an error when there is a tag but tags are not supported", () => { 90 | const reader = new StringReader("#minecraft:tag"); 91 | const result = parseNamespaceOrTag( 92 | reader, 93 | { 94 | ...blankproperties, 95 | suggesting: false 96 | }, 97 | errorBuilder 98 | ); 99 | if ( 100 | returnAssert(result, { 101 | errors: [ 102 | { 103 | code: "test", 104 | range: { start: 0, end: 14 } 105 | } 106 | ], 107 | succeeds: false 108 | }) 109 | ) { 110 | assert.strictEqual(result.data, undefined); 111 | } 112 | }); 113 | it("should fail when there is a tag which is unknown", () => { 114 | const reader = new StringReader("#minecraft:tag"); 115 | const result = parseNamespaceOrTag(reader, data, "block_tags"); 116 | if ( 117 | returnAssert(result, { 118 | succeeds: false 119 | }) 120 | ) { 121 | assert.deepStrictEqual(result.data, { 122 | namespace: "minecraft", 123 | path: "tag" 124 | }); 125 | } 126 | }); 127 | it("should resolve the tag successfully", () => { 128 | const reader = new StringReader("#minecraft:tick"); 129 | const result = parseNamespaceOrTag(reader, data, "block_tags"); 130 | if (returnAssert(result, { succeeds: true })) { 131 | assert.deepStrictEqual(result.data.parsed, { 132 | namespace: "minecraft", 133 | path: "tick" 134 | }); 135 | assertNamespaces( 136 | [ 137 | convertToResource("minecraft:stone"), 138 | convertToResource("minecraft:white_wool"), 139 | convertToResource("minecraft:red_wool") 140 | ], 141 | result.data.resolved 142 | ); 143 | } 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /src/test/parsers/brigadier/bool.test.ts: -------------------------------------------------------------------------------- 1 | import { boolParser } from "../../../parsers/brigadier"; 2 | import { testParser } from "../../assertions"; 3 | 4 | const boolTester = testParser(boolParser); 5 | const testWithBasicProps = boolTester(); 6 | 7 | describe("Boolean Argument Parser", () => { 8 | describe("parse()", () => { 9 | it("should not error when it is reading true", () => { 10 | testWithBasicProps("true", { 11 | succeeds: true, 12 | suggestions: ["true"] 13 | }); 14 | }); 15 | it("should not error when it is reading false", () => { 16 | testWithBasicProps("false", { 17 | succeeds: true, 18 | suggestions: ["false"] 19 | }); 20 | }); 21 | it("should error if it is not reading true or false", () => { 22 | testWithBasicProps("notbool", { 23 | errors: [ 24 | { 25 | code: "parsing.bool.invalid", 26 | range: { start: 0, end: 7 } 27 | } 28 | ], 29 | succeeds: false 30 | }); 31 | }); 32 | }); 33 | it("should suggest false for a string beginning with it", () => { 34 | testWithBasicProps("fal", { 35 | errors: [ 36 | { 37 | code: "parsing.bool.invalid", 38 | range: { start: 0, end: 3 } 39 | } 40 | ], 41 | succeeds: false, 42 | suggestions: ["false"] 43 | }); 44 | }); 45 | it("should suggest true for a string beginning with it", () => { 46 | testWithBasicProps("tru", { 47 | errors: [ 48 | { 49 | code: "parsing.bool.invalid", 50 | range: { start: 0, end: 3 } 51 | } 52 | ], 53 | succeeds: false, 54 | suggestions: ["true"] 55 | }); 56 | }); 57 | it("should suggest both for an empty string", () => { 58 | testWithBasicProps("", { 59 | errors: [ 60 | { 61 | code: "parsing.bool.invalid", 62 | range: { start: 0, end: 0 } 63 | } 64 | ], 65 | succeeds: false, 66 | suggestions: ["true", "false"] 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/test/parsers/brigadier/double.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { doubleParser } from "../../../parsers/brigadier/double"; 4 | import { testParser } from "../../assertions"; 5 | import { blankproperties } from "../../blanks"; 6 | 7 | const doubleTester = testParser(doubleParser); 8 | describe("Double Argument Parser", () => { 9 | function validDoubleTests( 10 | s: string, 11 | expectedNum: number, 12 | numEnd: number 13 | ): void { 14 | it("should succeed with no constraints", () => { 15 | const result = doubleTester(blankproperties)(s, { 16 | succeeds: true 17 | }); 18 | assert.strictEqual(result[1].cursor, numEnd); 19 | }); 20 | it("should reject a number less than the minimum", () => { 21 | const result = doubleTester({ 22 | ...blankproperties, 23 | node_properties: { min: expectedNum + 1 } 24 | })(s, { 25 | errors: [ 26 | { 27 | code: "argument.double.low", 28 | range: { start: 0, end: numEnd } 29 | } 30 | ], 31 | succeeds: true 32 | }); 33 | assert.strictEqual(result[1].cursor, numEnd); 34 | }); 35 | it("should reject a number more than the maximum", () => { 36 | const result = doubleTester({ 37 | ...blankproperties, 38 | node_properties: { 39 | max: expectedNum - 1 40 | } 41 | })(s, { 42 | errors: [ 43 | { 44 | code: "argument.double.big", 45 | range: { 46 | end: numEnd, 47 | start: 0 48 | } 49 | } 50 | ], 51 | succeeds: true 52 | }); 53 | assert.strictEqual(result[1].cursor, numEnd); 54 | }); 55 | } 56 | describe("valid integer", () => { 57 | validDoubleTests("1234", 1234, 4); 58 | }); 59 | describe("valid integer with space", () => { 60 | validDoubleTests("1234 ", 1234, 4); 61 | }); 62 | describe("valid float with `.`", () => { 63 | validDoubleTests("1234.5678", 1234.5678, 9); 64 | }); 65 | describe("valid float with `.` and space", () => { 66 | validDoubleTests("1234.5678 ", 1234.5678, 9); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/test/parsers/brigadier/float.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { floatParser } from "../../../parsers/brigadier"; 4 | import { testParser } from "../../assertions"; 5 | import { blankproperties } from "../../blanks"; 6 | 7 | const floatTester = testParser(floatParser); 8 | describe("Float Argument Parser", () => { 9 | function validFloatTests( 10 | s: string, 11 | expectedNum: number, 12 | numEnd: number 13 | ): void { 14 | it("should succeed with no constraints", () => { 15 | const result = floatTester(blankproperties)(s, { 16 | succeeds: true 17 | }); 18 | assert.strictEqual(result[1].cursor, numEnd); 19 | }); 20 | it("should reject a number less than the minimum", () => { 21 | const result = floatTester({ 22 | ...blankproperties, 23 | node_properties: { min: expectedNum + 1 } 24 | })(s, { 25 | errors: [ 26 | { 27 | code: "argument.float.low", 28 | range: { start: 0, end: numEnd } 29 | } 30 | ], 31 | succeeds: true 32 | }); 33 | assert.strictEqual(result[1].cursor, numEnd); 34 | }); 35 | it("should reject a number more than the maximum", () => { 36 | const result = floatTester({ 37 | ...blankproperties, 38 | node_properties: { 39 | max: expectedNum - 1 40 | } 41 | })(s, { 42 | errors: [ 43 | { 44 | code: "argument.float.big", 45 | range: { 46 | end: numEnd, 47 | start: 0 48 | } 49 | } 50 | ], 51 | succeeds: true 52 | }); 53 | assert.strictEqual(result[1].cursor, numEnd); 54 | }); 55 | } 56 | describe("valid integer", () => { 57 | validFloatTests("1234", 1234, 4); 58 | }); 59 | describe("valid integer with space", () => { 60 | validFloatTests("1234 ", 1234, 4); 61 | }); 62 | describe("valid float with `.`", () => { 63 | validFloatTests("1234.5678", 1234.5678, 9); 64 | }); 65 | describe("valid float with `.` and space", () => { 66 | validFloatTests("1234.5678 ", 1234.5678, 9); 67 | }); 68 | it("should fail when the number is bigger than the java maximum float", () => { 69 | floatTester(blankproperties)( 70 | "1000000000000000000000000000000000000000000000000000000000000", 71 | { 72 | errors: [ 73 | { 74 | code: "argument.float.big", 75 | range: { start: 0, end: 61 } 76 | } 77 | ], 78 | succeeds: true 79 | } 80 | ); 81 | }); 82 | it("should fail when the number is less than the java minimum float", () => { 83 | floatTester(blankproperties)( 84 | "-1000000000000000000000000000000000000000000000000000000000000", 85 | { 86 | errors: [ 87 | { 88 | code: "argument.float.low", 89 | range: { start: 0, end: 62 } 90 | } 91 | ], 92 | succeeds: true 93 | } 94 | ); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/test/parsers/brigadier/integer.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { intParser } from "../../../parsers/brigadier"; 4 | import { testParser } from "../../assertions"; 5 | 6 | const integerTest = testParser(intParser); 7 | 8 | describe("Integer Argument Parser", () => { 9 | function validIntTests( 10 | s: string, 11 | expectedNum: number, 12 | numEnd: number 13 | ): void { 14 | it("should succeed with an unconstrained value", () => { 15 | const result = integerTest()(s, { 16 | succeeds: true 17 | }); 18 | assert.strictEqual(result[1].cursor, numEnd); 19 | }); 20 | it("should fail with a value less than the minimum", () => { 21 | integerTest({ 22 | node_properties: { min: expectedNum + 1 } 23 | })(s, { 24 | errors: [ 25 | { 26 | code: "argument.integer.low", 27 | range: { start: 0, end: numEnd } 28 | } 29 | ], 30 | succeeds: true 31 | }); 32 | }); 33 | it("should fail with a value more than the maximum", () => { 34 | integerTest({ 35 | node_properties: { max: expectedNum - 1 } 36 | })(s, { 37 | errors: [ 38 | { 39 | code: "argument.integer.big", 40 | range: { start: 0, end: numEnd } 41 | } 42 | ], 43 | succeeds: true 44 | }); 45 | }); 46 | } 47 | describe("valid integer", () => { 48 | validIntTests("1234", 1234, 4); 49 | }); 50 | describe("valid integer with space", () => { 51 | validIntTests("1234 ", 1234, 4); 52 | }); 53 | it("should fail when the integer is bigger than the java max", () => { 54 | integerTest()("1000000000000000", { 55 | errors: [ 56 | { 57 | code: "argument.integer.big", 58 | range: { start: 0, end: 16 } 59 | } 60 | ], 61 | succeeds: true 62 | }); 63 | }); 64 | it("should fail when the integer is less than the java min", () => { 65 | integerTest()("-1000000000000000", { 66 | errors: [ 67 | { 68 | code: "argument.integer.low", 69 | range: { start: 0, end: 17 } 70 | } 71 | ], 72 | succeeds: true 73 | }); 74 | }); 75 | it("should fail when there is an invalid integer", () => { 76 | integerTest()("notint", { 77 | errors: [ 78 | { code: "parsing.int.expected", range: { start: 0, end: 6 } } 79 | ], 80 | succeeds: false 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/test/parsers/brigadier/string.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { stringParser } from "../../../parsers/brigadier"; 4 | import { testParser } from "../../assertions"; 5 | import { succeeds } from "../../blanks"; 6 | 7 | const stringTest = testParser(stringParser); 8 | 9 | describe("String Argument Parser", () => { 10 | describe("parse()", () => { 11 | it("should read to the end with a greedy string", () => { 12 | const result = stringTest({ 13 | node_properties: { type: "greedy" } 14 | })('test space :"-)(*', succeeds); 15 | assert.strictEqual(result[1].cursor, 17); 16 | }); 17 | describe("Phrase String", () => { 18 | const tester = stringTest({ node_properties: { type: "phrase" } }); 19 | it("should read an unquoted string section", () => { 20 | const result = tester('test space :"-)(*', succeeds); 21 | assert.strictEqual(result[1].cursor, 4); 22 | }); 23 | it("should read a quoted string section", () => { 24 | const result = tester('"quote test" :"-)(*', succeeds); 25 | assert.strictEqual(result[1].cursor, 12); 26 | }); 27 | }); 28 | describe("Word String", () => { 29 | const tester = stringTest({ node_properties: { type: "word" } }); 30 | it("should read only an unquoted string section", () => { 31 | const result = tester('test space :"-)(*', succeeds); 32 | assert.strictEqual(result[1].cursor, 4); 33 | }); 34 | it("should not read a quoted string section", () => { 35 | const result = tester('"quote test" :"-)(*', succeeds); 36 | assert.strictEqual(result[1].cursor, 0); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/test/parsers/get-parser.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { stringParser } from "../../parsers/brigadier"; 4 | import { getParser } from "../../parsers/get-parser"; 5 | import { literalParser } from "../../parsers/literal"; 6 | 7 | import { dummyParser } from "./tests/dummy1"; 8 | 9 | describe("getParser()", () => { 10 | it("should give the literal parser for a literal node", () => { 11 | const parser = getParser({ type: "literal" }); 12 | assert.strictEqual(parser, literalParser); 13 | }); 14 | it("should give the correct parser for a custom parser", () => { 15 | global.mcLangSettings = ({ 16 | parsers: { 17 | "langserver:dummy1": dummyParser 18 | } 19 | } as any) as McFunctionSettings; 20 | const parser = getParser({ 21 | parser: "langserver:dummy1", 22 | type: "argument" 23 | }); 24 | assert.strictEqual(parser, dummyParser); 25 | }); 26 | it("should give the correct parser for a builtin parser", () => { 27 | const parser = getParser({ 28 | parser: "brigadier:string", 29 | type: "argument" 30 | }); 31 | assert.strictEqual(parser, stringParser); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/test/parsers/literal.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { literalParser } from "../../parsers/literal"; 4 | import { testParser } from "../assertions"; 5 | 6 | const literalTest = testParser(literalParser)({ path: ["test"] }); 7 | 8 | describe("Literal Argument Parser", () => { 9 | describe("literal correct", () => { 10 | it("should succeed, suggesting the string", () => { 11 | const result = literalTest("test", { 12 | succeeds: true, 13 | suggestions: ["test"] 14 | }); 15 | assert.strictEqual(result[1].cursor, 4); 16 | }); 17 | it("should set the cursor to after the string when it doesn't reach the end", () => { 18 | const result = literalTest("test ", { 19 | succeeds: true 20 | }); 21 | assert.strictEqual(result[1].cursor, 4); 22 | }); 23 | }); 24 | describe("literal not matching", () => { 25 | it("should fail when the first character doesn't match", () => { 26 | literalTest("fail ", { 27 | succeeds: false 28 | }); 29 | }); 30 | it("should fail when the last character doesn't match", () => { 31 | literalTest("tesnot", { 32 | succeeds: false 33 | }); 34 | }); 35 | it("should suggest the string if the start is given", () => { 36 | literalTest("tes", { 37 | succeeds: false, 38 | suggestions: ["test"] 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/coord.test.ts: -------------------------------------------------------------------------------- 1 | import { CoordParser } from "../../../parsers/minecraft/coordinates"; 2 | import { testParser } from "../../assertions"; 3 | import { succeeds } from "../../blanks"; 4 | 5 | describe("Coordinate tests", () => { 6 | describe("parse()", () => { 7 | describe("settings: {count: 2, float: false, local: true }", () => { 8 | const parser = new CoordParser({ 9 | count: 2, 10 | float: false, 11 | local: true 12 | }); 13 | const tester = testParser(parser)(); 14 | ["~1 ~", "2 3", "5 ~", "~ ~"].forEach(v => 15 | it(`should work for coord ${v}`, () => { 16 | tester(v, succeeds); 17 | }) 18 | ); 19 | it("should return a mix error for coord '1 ^4'", () => { 20 | tester("1 ^4", { 21 | errors: [ 22 | { 23 | code: "argument.pos.mixed", 24 | range: { 25 | end: 4, 26 | start: 2 27 | } 28 | } 29 | ], 30 | succeeds: true 31 | }); 32 | }); 33 | it("should return a mix error for coord '~1 ^4'", () => { 34 | tester("~1 ^4", { 35 | errors: [ 36 | { 37 | code: "argument.pos.mixed", 38 | range: { 39 | end: 5, 40 | start: 3 41 | } 42 | } 43 | ], 44 | succeeds: true 45 | }); 46 | }); 47 | it("should return an incomplete error for coord '~2 '", () => { 48 | tester("~2 ", { 49 | errors: [ 50 | { 51 | code: "argument.pos.incomplete", 52 | range: { 53 | end: 3, 54 | start: 0 55 | } 56 | } 57 | ], 58 | succeeds: false, 59 | suggestions: [ 60 | { 61 | start: 3, 62 | text: "~" 63 | } 64 | ] 65 | }); 66 | }); 67 | it("should return an invalid int error for coord '1.3 1'", () => { 68 | tester("1.3 1", { 69 | errors: [ 70 | { 71 | code: "parsing.int.invalid", 72 | range: { 73 | end: 3, 74 | start: 0 75 | } 76 | } 77 | ], 78 | succeeds: false 79 | }); 80 | }); 81 | }); 82 | describe("settings: {count: 3, float: true , local: true }", () => { 83 | const parser = new CoordParser({ 84 | count: 3, 85 | float: true, 86 | local: true 87 | }); 88 | const tester = testParser(parser)(); 89 | [ 90 | "1 2 3", 91 | "~1 ~2 3", 92 | "^1 ^ ^2", 93 | "~ ~ ~", 94 | "1.2 3 ~1", 95 | "^.1 ^ ^3" 96 | ].forEach(v => 97 | it(`sould work for coord ${v}`, () => { 98 | tester(v, succeeds); 99 | }) 100 | ); 101 | }); 102 | describe("settings: {count: 2, float: true , local: false}", () => { 103 | const parser = new CoordParser({ 104 | count: 2, 105 | float: true, 106 | local: false 107 | }); 108 | const tester = testParser(parser)(); 109 | ["~1 ~", "2 3", "5 ~20", "~ ~"].forEach(v => 110 | it(`should work for coord ${v}`, () => { 111 | tester(v, succeeds); 112 | }) 113 | ); 114 | it("should return a no local error for coord '^ ^3'", () => { 115 | tester("^ ^3", { 116 | errors: [ 117 | { 118 | code: "argument.pos.nolocal", 119 | range: { 120 | end: 1, 121 | start: 0 122 | } 123 | }, 124 | { 125 | code: "argument.pos.nolocal", 126 | range: { 127 | end: 3, 128 | start: 2 129 | } 130 | } 131 | ], 132 | succeeds: true 133 | }); 134 | }); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/lists.test.ts: -------------------------------------------------------------------------------- 1 | import { CommandErrorBuilder } from "../../../brigadier/errors"; 2 | import { ListParser } from "../../../parsers/minecraft/lists"; 3 | import { testParser } from "../../assertions"; 4 | 5 | const list = ["foo", "bar", "baz", "hello", "world"]; 6 | 7 | describe("list tests", () => { 8 | describe("parse()", () => { 9 | const parser = new ListParser( 10 | list, 11 | new CommandErrorBuilder("arg.ex", "example error") 12 | ); 13 | 14 | const tester = testParser(parser)(); 15 | list.forEach(v => 16 | it(`should parse '${v}' correctly`, () => { 17 | tester(v, { 18 | succeeds: true, 19 | suggestions: [v] 20 | }); 21 | }) 22 | ); 23 | it("should fail for a different input", () => { 24 | tester("badinput", { 25 | errors: [ 26 | { 27 | code: "arg.ex", 28 | range: { 29 | end: 8, 30 | start: 0 31 | } 32 | } 33 | ], 34 | succeeds: false 35 | }); 36 | }); 37 | it("should suggest all values for an empty string", () => { 38 | tester("", { 39 | errors: [ 40 | { 41 | code: "arg.ex", 42 | range: { 43 | end: 0, 44 | start: 0 45 | } 46 | } 47 | ], 48 | succeeds: false, 49 | suggestions: list 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/message.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { StringReader } from "../../../brigadier/string-reader"; 4 | import { messageParser } from "../../../parsers/minecraft/message"; 5 | import { ParserInfo } from "../../../types"; 6 | 7 | describe("Message parser", () => { 8 | it("should set the cursor to the correct position", () => { 9 | const reader = new StringReader("say this is a super fun string"); 10 | reader.cursor = 4; 11 | messageParser.parse(reader, {} as ParserInfo); 12 | assert.strictEqual(reader.cursor, 30); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/namespace-list.test.ts: -------------------------------------------------------------------------------- 1 | import { CommandErrorBuilder } from "../../../brigadier/errors"; 2 | import { NamespaceListParser } from "../../../parsers/minecraft/namespace-list"; 3 | import { testParser } from "../../assertions"; 4 | 5 | const parser = new NamespaceListParser( 6 | "minecraft:biome", 7 | new CommandErrorBuilder("namespace.test.unknown", "Unkown") 8 | ); 9 | 10 | const tester = testParser(parser)({ 11 | data: { 12 | globalData: { 13 | registries: { 14 | "minecraft:biome": [ 15 | "minecraft:test", 16 | "minecraft:test2", 17 | "minecraft:other", 18 | "something:hello" 19 | ] 20 | } 21 | } 22 | } 23 | } as any); 24 | 25 | describe("NamespaceListParser", () => { 26 | it("should allow a known namespace", () => { 27 | tester("minecraft:test", { 28 | succeeds: true, 29 | suggestions: ["minecraft:test", "minecraft:test2"] 30 | }); 31 | }); 32 | it("should accept a non-minecraft namespace", () => { 33 | tester("something:hello", { 34 | succeeds: true, 35 | suggestions: ["something:hello"] 36 | }); 37 | }); 38 | it("should give the given error for an incorrect namespace", () => { 39 | tester("unknown:unknown", { 40 | errors: [ 41 | { 42 | code: "namespace.test.unknown", 43 | range: { start: 0, end: 15 } 44 | } 45 | ], 46 | succeeds: true 47 | }); 48 | }); 49 | it("should fail for an unparseable namespace", () => { 50 | tester("fail:fail:fail", { 51 | errors: [ 52 | { code: "argument.id.invalid", range: { start: 9, end: 10 } } 53 | ], 54 | succeeds: false 55 | }); 56 | }); 57 | it("should suggest all values for an empty string", () => { 58 | tester("", { 59 | errors: [ 60 | { code: "namespace.test.unknown", range: { start: 0, end: 0 } } 61 | ], 62 | succeeds: true, 63 | suggestions: [ 64 | "minecraft:test", 65 | "minecraft:test2", 66 | "minecraft:other", 67 | "something:hello" 68 | ] 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/nbt/doc-walker.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { CompoundNode } from "mc-nbt-paths"; 3 | 4 | import { NBTTagCompound } from "../../../../parsers/minecraft/nbt/tag/compound-tag"; 5 | import { NBTTagString } from "../../../../parsers/minecraft/nbt/tag/string-tag"; 6 | import { NodeInfo } from "../../../../parsers/minecraft/nbt/util/doc-walker-util"; 7 | import { NBTWalker } from "../../../../parsers/minecraft/nbt/walker"; 8 | 9 | import { testDocs } from "./test-data"; 10 | 11 | const test = (walker: NBTWalker, name: string, extpath: string[] = []) => { 12 | const node = walker.getInitialNode([name, ...extpath]); 13 | assert.strictEqual(node.node.description, `${name} OK`); 14 | }; 15 | 16 | describe("Documentation Walker Tests", () => { 17 | describe("getInitialNode()", () => { 18 | const walker = new NBTWalker(testDocs); 19 | 20 | it("should return the correct node for the basic doc", () => { 21 | test(walker, "basic_test"); 22 | }); 23 | 24 | it("should return the correct node for nested nodes", () => { 25 | test(walker, "nest_test", ["key0"]); 26 | }); 27 | 28 | it("should return the correct node for the ref property", () => { 29 | test(walker, "ref_test"); 30 | }); 31 | 32 | it("should return the correct node for the child_ref property", () => { 33 | test(walker, "child_ref_test", ["key0"]); 34 | }); 35 | 36 | it("should return the correct node for the child_ref property but self", () => { 37 | test(walker, "child_ref_self_test", ["key1"]); 38 | }); 39 | 40 | it("should return the correct node for lists", () => { 41 | test(walker, "list_test", ["0"]); 42 | }); 43 | 44 | it.skip("should return the correct node for funcs", () => { 45 | // TODO: Rework 46 | const map = new Map(); 47 | map.set("var1", new NBTTagString(["var1"]).setValue("func_test")); 48 | // @ts-ignore 49 | // tslint:disable-next-line:variable-name 50 | const _nbt = new NBTTagCompound([]).setValue(map); 51 | test(walker, "func_test"); 52 | }); 53 | 54 | it("should return the correct node for ref pointing to 'references'", () => { 55 | test(walker, "ref_references_test"); 56 | }); 57 | 58 | it("should merge child_ref correctly", () => { 59 | const node = walker.getInitialNode(["child_ref_test"]); 60 | const children = walker.getChildren(node as NodeInfo); 61 | assert.deepStrictEqual(children, { 62 | bad: { 63 | node: { 64 | description: "child_ref_test BAD", 65 | type: "no-nbt" 66 | }, 67 | path: "root.json" 68 | }, 69 | badkey: { 70 | node: { 71 | description: "child_ref_test BAD", 72 | type: "no-nbt" 73 | }, 74 | path: "child_ref_test.json" 75 | }, 76 | key0: { 77 | node: { 78 | description: "child_ref_test OK", 79 | type: "no-nbt" 80 | }, 81 | path: "child_ref_test.json" 82 | } 83 | }); 84 | }); 85 | 86 | it("should return the correct node for root node groups", () => { 87 | test(walker, "root_group_test", ["key3"]); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/nbt/tag-parser-data.ts: -------------------------------------------------------------------------------- 1 | import { NBTDocs } from "../../../../data/types"; 2 | 3 | export const docs: NBTDocs = new Map(); 4 | 5 | docs.set("byte_test_succeed", { 6 | type: "byte" 7 | }); 8 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/nbt/tag-parser.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { NBTNode } from "mc-nbt-paths"; 3 | 4 | import { StringReader } from "../../../../brigadier/string-reader"; 5 | import { NBTTag, TagType } from "../../../../parsers/minecraft/nbt/tag/nbt-tag"; 6 | import { NBTTagNumber } from "../../../../parsers/minecraft/nbt/tag/number"; 7 | import { Correctness } from "../../../../parsers/minecraft/nbt/util/nbt-util"; 8 | import { NBTWalker } from "../../../../parsers/minecraft/nbt/walker"; 9 | import { returnAssert, ReturnAssertionInfo } from "../../../assertions"; 10 | 11 | interface ParseTest { 12 | expected: ReturnAssertionInfo; 13 | text: string; 14 | } 15 | 16 | interface ValidateTest { 17 | expected: ReturnAssertionInfo; 18 | node: NBTNode; 19 | walker?: NBTWalker; 20 | } 21 | 22 | interface ValueTest { 23 | type?: TagType; 24 | value: any; 25 | } 26 | 27 | const testTag = ( 28 | tag: NBTTag, 29 | parse: ParseTest, 30 | validate?: ValidateTest, 31 | value?: ValueTest 32 | ) => { 33 | const response = tag.parse(new StringReader(parse.text)); 34 | returnAssert(response, parse.expected); 35 | if ( 36 | response.data === Correctness.MAYBE || 37 | response.data === Correctness.CERTAIN 38 | ) { 39 | if (validate) { 40 | const valres = tag.validate( 41 | { path: "", node: validate.node }, 42 | validate.walker || new NBTWalker(new Map()) 43 | ); 44 | returnAssert(valres, validate.expected); 45 | } 46 | if (value) { 47 | assert.deepStrictEqual(tag.getValue(), value.value); 48 | assert.strictEqual( 49 | // @ts-ignore 50 | tag.tagType, 51 | value.type 52 | ); 53 | } 54 | } 55 | }; 56 | 57 | describe("SNBT Tag Parser Tests", () => { 58 | describe("Byte Tag", () => { 59 | it("should parse correctly", () => 60 | testTag( 61 | new NBTTagNumber([]), 62 | { 63 | expected: { 64 | succeeds: true 65 | }, 66 | text: "123b" 67 | }, 68 | { 69 | expected: { 70 | succeeds: true 71 | }, 72 | node: { 73 | type: "byte" 74 | } 75 | }, 76 | { 77 | value: 123 78 | } 79 | )); 80 | it("should parse as a short and fail validation", () => 81 | testTag( 82 | new NBTTagNumber([]), 83 | { 84 | expected: { 85 | succeeds: true 86 | }, 87 | text: "321s" 88 | }, 89 | { 90 | expected: { 91 | succeeds: true 92 | }, 93 | node: { 94 | type: "short" 95 | } 96 | } 97 | )); 98 | it("should give an invalid number error", () => 99 | testTag(new NBTTagNumber([]), { 100 | expected: { 101 | errors: [ 102 | { 103 | code: "parsing.float.expected", 104 | range: { 105 | end: 5, 106 | start: 0 107 | } 108 | } 109 | ], 110 | succeeds: false 111 | }, 112 | text: "hello" 113 | })); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/range.test.ts: -------------------------------------------------------------------------------- 1 | import { floatRange, intRange } from "../../../parsers/minecraft/range"; 2 | import { testParser } from "../../assertions"; 3 | 4 | describe("range parser", () => { 5 | describe("int range", () => { 6 | const tester = testParser(intRange)(); 7 | it("should succeed with a single int", () => { 8 | tester("10", { 9 | succeeds: true, 10 | suggestions: [ 11 | { 12 | start: 2, 13 | text: ".." 14 | } 15 | ] 16 | }); 17 | }); 18 | it("should succeed with a right unbounded range", () => { 19 | tester("3..", { 20 | succeeds: true, 21 | suggestions: [ 22 | { 23 | start: 1, 24 | text: ".." 25 | } 26 | ] 27 | }); 28 | }); 29 | it("should succeed with a left unbounded range", () => { 30 | tester("..34", { 31 | succeeds: true 32 | }); 33 | }); 34 | it("should succeed with a bounded range", () => { 35 | tester("12..34", { 36 | succeeds: true 37 | }); 38 | }); 39 | it("should fail when no numbers", () => { 40 | tester("..", { 41 | errors: [ 42 | { 43 | code: "parsing.int.expected", 44 | range: { 45 | end: 2, 46 | start: 2 47 | } 48 | } 49 | ], 50 | succeeds: false, 51 | suggestions: [ 52 | { 53 | start: 0, 54 | text: ".." 55 | } 56 | ] 57 | }); 58 | }); 59 | it("should have errors when min as greater than max", () => { 60 | tester("7..-12", { 61 | actions: [ 62 | { 63 | data: "-12..7", 64 | high: 6, 65 | low: 0, 66 | type: "format" 67 | } 68 | ], 69 | errors: [ 70 | { 71 | code: "argument.range.swapped", 72 | range: { 73 | end: 6, 74 | start: 0 75 | } 76 | } 77 | ], 78 | succeeds: true 79 | }); 80 | }); 81 | it("should have a hint when min equals max", () => { 82 | tester("5..5", { 83 | actions: [ 84 | { 85 | data: "5", 86 | high: 4, 87 | low: 0, 88 | type: "format" 89 | } 90 | ], 91 | errors: [ 92 | { 93 | code: "argument.range.equal", 94 | range: { 95 | end: 4, 96 | start: 0 97 | } 98 | } 99 | ], 100 | succeeds: true 101 | }); 102 | }); 103 | }); 104 | describe("float range", () => { 105 | const tester = testParser(floatRange)(); 106 | it("should succeed with a single float", () => { 107 | tester("9.32", { 108 | succeeds: true, 109 | suggestions: [ 110 | { 111 | start: 4, 112 | text: ".." 113 | } 114 | ] 115 | }); 116 | }); 117 | it("should succeed with a right unbounded range", () => { 118 | tester("3.12..", { 119 | succeeds: true, 120 | suggestions: [ 121 | { 122 | start: 4, 123 | text: ".." 124 | } 125 | ] 126 | }); 127 | }); 128 | it("should succeed with a left unbounded range & float without starting '0'", () => { 129 | tester("...4", { 130 | succeeds: true 131 | }); 132 | }); 133 | it("should have errors when min as greater than max", () => { 134 | tester("3.4..0.2", { 135 | actions: [ 136 | { 137 | data: "0.2..3.4", 138 | high: 8, 139 | low: 0, 140 | type: "format" 141 | } 142 | ], 143 | errors: [ 144 | { 145 | code: "argument.range.swapped", 146 | range: { 147 | end: 8, 148 | start: 0 149 | } 150 | } 151 | ], 152 | succeeds: true 153 | }); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/resources.test.ts: -------------------------------------------------------------------------------- 1 | import * as resourceParsers from "../../../parsers/minecraft/resources"; 2 | import { convertToResource, testParser } from "../../assertions"; 3 | 4 | const functionTester = testParser(resourceParsers.functionParser)({ 5 | data: { 6 | globalData: { 7 | resources: { 8 | function_tags: [convertToResource("minecraft:tag")], 9 | functions: [ 10 | convertToResource("minecraft:test"), 11 | convertToResource("minecraft:test2") 12 | ] 13 | } 14 | } 15 | } 16 | } as any); 17 | 18 | describe("function parser", () => { 19 | it("should accept a known function", () => { 20 | functionTester("minecraft:test", { 21 | succeeds: true, 22 | suggestions: ["minecraft:test", "minecraft:test2"] 23 | }); 24 | }); 25 | it("should give an error for an unknown function", () => { 26 | functionTester("minecraft:unknown", { 27 | errors: [ 28 | { 29 | code: "arguments.function.unknown", 30 | range: { start: 0, end: 17 } 31 | } 32 | ], 33 | succeeds: true 34 | }); 35 | }); 36 | it("should allow a known tag", () => { 37 | functionTester("#minecraft:tag", { 38 | succeeds: true, 39 | suggestions: [{ start: 1, text: "minecraft:tag" }] 40 | }); 41 | }); 42 | it("should give an error for an unknown tag", () => { 43 | functionTester("#minecraft:unknowntag", { 44 | errors: [ 45 | { 46 | code: "arguments.function.tag.unknown", 47 | range: { start: 0, end: 21 } 48 | } 49 | ], 50 | succeeds: true 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/scoreboard.test.ts: -------------------------------------------------------------------------------- 1 | import { criteriaParser } from "../../../parsers/minecraft/scoreboard"; 2 | import { testParser } from "../../assertions"; 3 | 4 | describe("scoreboard parsers", () => { 5 | describe("criterion parser", () => { 6 | const test = testParser(criteriaParser)({ 7 | data: { 8 | globalData: { 9 | registries: { 10 | "minecraft:custom_stat": new Set([ 11 | "minecraft:some_custom" 12 | ]), 13 | "minecraft:item": new Set([ 14 | "minecraft:some_item", 15 | "minecraft:some_other_item" 16 | ]) 17 | } 18 | } 19 | } as any 20 | }); 21 | it("should support parsing a simple criteria", () => { 22 | test("air", { succeeds: true, suggestions: ["air"] }); 23 | }); 24 | it("should support a color requiring criterion", () => { 25 | test("teamkill.aqua", { 26 | start: 9, 27 | succeeds: true, 28 | suggestions: ["aqua"] 29 | }); 30 | }); 31 | it("should support a custom criterion", () => { 32 | test("minecraft.custom:minecraft.some_custom", { 33 | start: 17, 34 | succeeds: true, 35 | suggestions: ["minecraft.some_custom"] 36 | }); 37 | }); 38 | it("should support a custom criterion without the namespace", () => { 39 | test("minecraft.custom:some_custom", { 40 | start: 17, 41 | succeeds: true, 42 | suggestions: ["minecraft.some_custom"] 43 | }); 44 | }); 45 | it("should support a custom criterion without any namespace", () => { 46 | test("custom:some_custom", { 47 | start: 7, 48 | succeeds: true, 49 | suggestions: ["minecraft.some_custom"] 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/swizzle.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { StringReader } from "../../../brigadier/string-reader"; 4 | import { swizzleParer } from "../../../parsers/minecraft/swizzle"; 5 | import { ParserInfo, Suggestion } from "../../../types"; 6 | 7 | const prop: ParserInfo = {} as ParserInfo; 8 | 9 | describe("Swizzle", () => { 10 | describe("parse()", () => { 11 | ["z", "yx", "xyz"].forEach(v => 12 | it(`should not fail when parsing ${v}`, () => { 13 | const reader = new StringReader(v); 14 | const out = swizzleParer.parse(reader, prop); 15 | assert.ok(out.kind); 16 | }) 17 | ); 18 | [["xx", "x"], ["zyy", "y"], ["xyyz", "y"]].forEach(v => 19 | it(`should throw a duplicate exception when parsing ${ 20 | v[0] 21 | }`, () => { 22 | const reader = new StringReader(v[0]); 23 | const out = swizzleParer.parse(reader, prop); 24 | if (out.kind) { 25 | assert.fail("Did not throw an error"); 26 | } else { 27 | const ex = out.errors[0]; 28 | assert.strictEqual(ex.code, "argument.swizzle.duplicate"); 29 | assert.strictEqual( 30 | ex.text, 31 | `Duplicate character '${v[1]}'` 32 | ); 33 | } 34 | }) 35 | ); 36 | [ 37 | ["ax", "a"], 38 | ["xyb", "b"], 39 | ["xxf", "f"] // Invalid character checking should come before duplicate checking 40 | ].forEach(v => 41 | it(`should throw an invalid character exception when parsing ${ 42 | v[0] 43 | }`, () => { 44 | const reader = new StringReader(v[0]); 45 | const out = swizzleParer.parse(reader, prop); 46 | if (out.kind) { 47 | assert.fail("Did not throw an error"); 48 | } else { 49 | const ex = out.errors[0]; 50 | assert.strictEqual(ex.code, "argument.swizzle.invalid"); 51 | assert.strictEqual(ex.text, `Invalid character '${v[1]}'`); 52 | } 53 | }) 54 | ); 55 | }); 56 | describe("suggestions", () => { 57 | [ 58 | ["", ["x", "y", "z", "xy", "xz", "yz", "xyz"]], 59 | ["z", ["z", "zx", "zy", "zxy"]], 60 | ["yx", ["yx", "yxz"]], 61 | ["xyz", ["xyz"]] 62 | ].forEach(v => 63 | it(`should return correct suggestion for ${v[0]}`, () => { 64 | const arg = swizzleParer.parse( 65 | new StringReader(v[0] as string), 66 | prop 67 | ); 68 | const out = arg.suggestions as Suggestion[]; 69 | assert.ok( 70 | out 71 | .map(v2 => v2) 72 | .every( 73 | v2 => (v[1] as string[]).indexOf(v2.text) !== -1 74 | ), 75 | `[${out 76 | .map(v2 => v2.text) 77 | .join(", ")}] does not match [${(v[1] as string[]).join( 78 | ", " 79 | )}]` 80 | ); 81 | }) 82 | ); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/test/parsers/minecraft/time.test.ts: -------------------------------------------------------------------------------- 1 | import { timeParser } from "../../../parsers/minecraft/time"; 2 | import { testParser } from "../../assertions"; 3 | 4 | const test = testParser(timeParser)(); 5 | 6 | describe("time parser", () => { 7 | it("should allow a plain integer", () => { 8 | test("123", { succeeds: true, suggestions: ["t", "s", "d"], start: 3 }); 9 | }); 10 | it("should allow an integer number of ticks", () => { 11 | test("123t", { start: 3, succeeds: true, suggestions: ["t"] }); 12 | }); 13 | it("should allow an integer number of seconds", () => { 14 | test("123s", { start: 3, succeeds: true, suggestions: ["s"] }); 15 | }); 16 | it("should allow a nice float number of seconds", () => { 17 | test("0.5s", { start: 3, succeeds: true, suggestions: ["s"] }); 18 | }); 19 | it("should not allow a negative integer", () => { 20 | test("-123", { 21 | errors: [ 22 | { 23 | code: "argument.time.not_nonnegative_integer", 24 | range: { start: 0, end: 4 } 25 | } 26 | ], 27 | start: 4, 28 | succeeds: true, 29 | suggestions: ["t", "s", "d"] 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/test/parsers/tests/dummy1.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | import { testParser } from "../../assertions"; 4 | 5 | import { dummyParser } from "./dummy1"; 6 | 7 | const dummyParserTester = testParser(dummyParser); 8 | 9 | describe("dummyParser1", () => { 10 | describe("parse", () => { 11 | it("should read the specified number of characters", () => { 12 | const result = dummyParserTester({ 13 | node_properties: { number: 4 } 14 | })("test hello", { 15 | succeeds: true, 16 | suggestions: ["hello", { text: "welcome", start: 2 }] 17 | }); 18 | assert.strictEqual(result[1].cursor, 4); 19 | }); 20 | 21 | it("should default to 3 when not given any properties", () => { 22 | const result = dummyParserTester()("test hello", { 23 | succeeds: true, 24 | suggestions: ["hello", { text: "welcome", start: 1 }] 25 | }); 26 | assert.strictEqual(result[1].cursor, 3); 27 | }); 28 | 29 | it("should not succeed if there is not enough room", () => { 30 | dummyParserTester()("te", { 31 | succeeds: false, 32 | suggestions: ["hello", { text: "welcome", start: 1 }] 33 | }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/test/parsers/tests/dummy1.ts: -------------------------------------------------------------------------------- 1 | import { ReturnHelper } from "../../../misc-functions"; 2 | import { Parser } from "../../../types"; 3 | 4 | /** 5 | * Used for testing. 6 | * Do not attempt to use an actual command tree using this. 7 | */ 8 | export const dummyParser: Parser = { 9 | parse: (reader, props) => { 10 | const helper = new ReturnHelper(props); 11 | const num: number = (props.node_properties.number as number) || 3; 12 | helper.addSuggestions("hello"); 13 | helper.addSuggestion( 14 | Math.min(Math.floor(num / 2), reader.string.length), 15 | "welcome" 16 | ); 17 | if (reader.canRead(num)) { 18 | reader.cursor += num; 19 | return helper.succeed(); 20 | } else { 21 | return helper.fail(); 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/test/untested.md: -------------------------------------------------------------------------------- 1 | # Needing tests 2 | 3 | Contributions are welcome to remove from this list. 4 | 5 | # Probably unneeded tests 6 | 7 | Things which can be tested, but it might not be necessary to test. 8 | 9 | Contributions are welcome to remove from this list. 10 | 11 | - [Security](../misc-functions/security.ts) - Very precise output of what is 12 | returned. Might change depending on settings. 13 | - [Return Helper](../misc-functions/returnhelper.ts) - Not very complex, but 14 | some assurances could be useful 15 | - [Translation](../misc-functions/translation.ts) - Not yet implemented. Might 16 | need to ensure that replacing is done correctly if %s parser is changed 17 | - [JSON parsing](../parsers/minecraft/json.ts) - No tests should be needed as 18 | we trust the `vscode-json-languageservice` library. 19 | 20 | # Untested sections 21 | 22 | These sections of the language server are untested, either through infeasibility 23 | or it not being needed. 24 | 25 | - [Setup logging](../misc-functions/setup.ts) - Cannot be easily tested as 26 | global `mclanglog` is already defined for Mocha through 27 | [logging_setup.ts](./logging_setup.ts) 28 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function equalValue(obj1: any, obj2: any): boolean { 2 | if (typeof obj1 !== typeof obj2) { 3 | return false; 4 | } else if (typeof obj1 !== "object") { 5 | return obj1 === obj2; 6 | } else { 7 | const obj1keys = Object.keys(obj1); 8 | const obj2keys = Object.keys(obj2); 9 | if (obj1keys.length !== obj2keys.length) { 10 | return false; 11 | } 12 | return obj1keys.every(v => equalValue(obj1[v], obj2[v])); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test_data/test_nbt/bigtest.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Levertion/mcfunction-langserver/3243503c2748b0978a43f92d613e2ceaf8e9712a/test_data/test_nbt/bigtest.nbt -------------------------------------------------------------------------------- /test_data/test_world/datapacks/ExampleDatapack/data/minecraft/tags/functions/tick.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": ["test_namespace:function"] 3 | } 4 | -------------------------------------------------------------------------------- /test_data/test_world/datapacks/ExampleDatapack/data/test_namespace/functions/function.mcfunction: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Levertion/mcfunction-langserver/3243503c2748b0978a43f92d613e2ceaf8e9712a/test_data/test_world/datapacks/ExampleDatapack/data/test_namespace/functions/function.mcfunction -------------------------------------------------------------------------------- /test_data/test_world/datapacks/ExampleDatapack/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "pack_format": 3, 4 | "description": "test datapack" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test_data/test_world/datapacks/ExampleDatapack2/data/test_namespace/functions/function.mcfunction: -------------------------------------------------------------------------------- 1 | # Bad conflict -------------------------------------------------------------------------------- /test_data/test_world/datapacks/ExampleDatapack2/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "pack_format": 3, 4 | "description": "test datapack 2" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test_data/test_world/level.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Levertion/mcfunction-langserver/3243503c2748b0978a43f92d613e2ceaf8e9712a/test_data/test_world/level.dat -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | ## Update README 2 | 3 | ### Add implementing section 4 | 5 | - Client must implement a handler for the `"mcfunction/shutdown"` 6 | notification, to stop the server gracefully from the server side. 7 | This notification should make the client send the server a shutdown and exit 8 | request. 9 | 10 | ### Custom protocol commands 11 | 12 | - `mcfunction/shutdown` (c <- s) (required): Client should call `shutdown` and 13 | `exit`. The single string argument is the error message to be shown. 14 | 15 | ## Highlighting 16 | 17 | Work out how the API for highlighting should look. 18 | 19 | This has been postponed until 20 | [microsoft/language-server-protocol#18](https://github.com/microsoft/language-server-protocol/issues/18) 21 | is resolved 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": [ 7 | "es2017" 8 | ] /* Specify library files to be included in the compilation: */, 9 | "outDir": "./out" /* Redirect output structure to the directory. */, 10 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 11 | /* Strict Type-Checking Options */ 12 | "strict": true /* Enable all strict type-checking options. */, 13 | /* Additional Checks */ 14 | "noUnusedLocals": true /* Report errors on unused locals. */, 15 | "noUnusedParameters": true /* Report errors on unused parameters. */, 16 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 17 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 18 | /* Source Map Options */ 19 | "sourceMap": true, 20 | "importHelpers": true 21 | }, 22 | "exclude": ["lintrules/**"] 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:all", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": [true, "never-prefix"], 5 | "no-floating-promises": true, 6 | "typedef": { 7 | "options": ["call-signature", "parameter", "property-declaration"] 8 | }, 9 | "no-magic-numbers": false, 10 | "completed-docs": false, 11 | "only-arrow-functions": { "options": "allow-declarations" }, 12 | "no-any": false, 13 | "newline-before-return": false, 14 | "strict-boolean-expressions": false, 15 | "no-object-literal-type-assertion": false, 16 | "no-unsafe-any": false, 17 | "restrict-plus-operands": false, 18 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 19 | "file-name-casing": [true, "kebab-case"], 20 | "no-submodule-imports": false, 21 | "helper-return": true, 22 | "max-file-line-count": false, 23 | "ban-ts-ignore": false, 24 | "increment-decrement": false 25 | }, 26 | "linterOptions": { 27 | "exclude": ["lintrules/**"] 28 | }, 29 | "rulesDirectory": "lintrules" 30 | } 31 | --------------------------------------------------------------------------------