├── .gitignore ├── .vscode └── launch.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── client ├── package-lock.json ├── package.json ├── src │ └── extension.ts └── tsconfig.json ├── json-schema ├── packdata.json ├── packlist.json ├── songdata.json ├── songlist.json ├── unlockdata.json └── unlocks.json ├── language-configuration.json ├── package-lock.json ├── package.json ├── server ├── package-lock.json ├── package.json ├── src │ ├── associated-data │ │ ├── allow-memes.ts │ │ ├── enwiden.ts │ │ └── timing.ts │ ├── checker │ │ ├── allow-memes.ts │ │ ├── arc-position.ts │ │ ├── cut-by-timing.ts │ │ ├── enwiden.ts │ │ ├── extra-lanes.ts │ │ ├── float-digit.ts │ │ ├── metadata.ts │ │ ├── overlap.ts │ │ ├── scenecontrol.ts │ │ ├── timing.ts │ │ ├── timinggroup-attribute.ts │ │ └── value-range.ts │ ├── checkers.ts │ ├── lang.ts │ ├── lexer.ts │ ├── parser.ts │ ├── server.ts │ ├── to-ast.ts │ ├── types.ts │ └── util │ │ ├── associated-data.ts │ │ └── misc.ts └── tsconfig.json ├── snippets.json ├── syntaxes └── arcaea-aff.tmLanguage.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | .vscode/settings.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 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 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .gitignore 4 | **/*.ts 5 | **/*.map 6 | .gitignore 7 | **/tsconfig.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.14.0 4 | 5 | - Support for new `scenecontrol` types 6 | - Check for non-positive duration `scenecontrol` 7 | - Support for `arc` with new line type 8 | - Update JSON validation for the `songlist` `packlist` `unlocks` files for the arcaea 6.0.x 9 | 10 | ## 0.13.1 11 | 12 | - Update `unlocks` schema for new unlock condition 13 | 14 | ## 0.13.0 15 | 16 | - Update JSON validation for the `songlist` `packlist` `unlocks` files for the eternal difficulty 17 | - Handle various size `arctap` represented by the `arc` with color of 3 18 | 19 | ## 0.12.3 20 | 21 | - Update JSON validation for the `songlist` `packlist` `unlocks` files for the arcaea 5.0.x 22 | 23 | ## 0.12.2 24 | 25 | - Update `songlist` schema for new fields for song searching 26 | 27 | ## 0.12.1 28 | 29 | - Update `unlocks` schema for new unlock condition 30 | 31 | ## 0.12.0 32 | 33 | - Support for special sound effect of `arctap` 34 | - Support for `tap` and `hold` on the track 0 and 5 35 | - Support for new `scenecontrol` types 36 | - Better way to handle the `arc` with color of 3 37 | - Check for overlapped `scenecontrol` 38 | - Use a more relaxing check for out of range `arc` when the enwiden mode is on 39 | - Update JSON validation for the `songlist` `packlist` `unlocks` files for the arcaea 4.0.x 40 | 41 | ## 0.11.2 42 | 43 | - Update `unlocks` schema to provide better description to fields 44 | 45 | ## 0.11.1 46 | 47 | - Update `unlocks` schema to provide better description to fields 48 | - Fix some typos in schema 49 | - Fix crashing when `scenecontrol` do not have a kind 50 | 51 | ## 0.11.0 52 | 53 | - Allow nested `camera` in `timinggroup` 54 | - Support and check for `timinggroup` attributes 55 | 56 | ## 0.10.7 57 | 58 | - Update `songlist` schema for fields used in 5 years anniversary update 59 | - Fix typo in schemas 60 | 61 | ## 0.10.6 62 | 63 | - Update `songlist` schema for fields used in link play update and PRAGMATISM -RESURRECTION- 64 | 65 | ## 0.10.5 66 | 67 | - Fix `$id` field of schema 68 | 69 | ## 0.10.4 70 | 71 | - Fix `songlist` schema for correct required fields in difficulties 72 | 73 | ## 0.10.3 74 | 75 | - Update `songlist` schema for hidden additional features 76 | - Update `songlist` `packlist` `unlocks` schema to provide better description to fields 77 | 78 | ## 0.10.2 79 | 80 | - `TimingPointDensityFactor` metadata is now checked 81 | 82 | ## 0.10.1 83 | 84 | - `scenecontrol` in `timinggroup` is now checked 85 | 86 | ## 0.10.0 87 | 88 | - Support for `timinggroup` event with `noinput` type 89 | - It's no longer an error to put `scenecontrol` into `timinggroup` 90 | - Add a setting to control the displayed diagnostic infomation 91 | 92 | ## 0.9.0 93 | 94 | - Update `songlist` schema for hidden additional features 95 | 96 | ## 0.8.0 97 | 98 | - Support for `timinggroup` event 99 | - Handle different kind of `scenecontrol` event better 100 | - Cut by timing is now info instead of error 101 | - Update JSON validation for the `songlist` `packlist` `unlocks` files for the arcaea 3.0.0 102 | - More generic syntax highlighting 103 | - Be more relax for whitespace and endline token 104 | 105 | ## 0.7.0 106 | 107 | - Support for `camera` and `scenecontrol` event 108 | - Check for `camera` duration 109 | - Check for `camera` duplicated 110 | - Disable check for out of range `arc` items when memes items found 111 | - Various fix in the `songlist` schema 112 | 113 | ## 0.6.1 114 | 115 | - Fix a description in the `unlocks` schema 116 | - Simplify development workflow 117 | 118 | ## 0.6.0 119 | 120 | - Update `unlocks` schema for new unlock conditions 121 | 122 | ## 0.5.0 123 | 124 | - Check for overlapped `arctap` and floor(track) items 125 | - Change the behaviour for `arctap` on solid `arc` 126 | - Update `songlist` schema for the new day-night feature 127 | - Various `songlist` schema fix 128 | 129 | ## 0.4.0 130 | 131 | - Unexpected whitespace will not block other checks 132 | - Check for out of range `arc` items 133 | 134 | ## 0.3.1 135 | 136 | - Clean diagnostics on close 137 | - Adjust severity of some problem reports 138 | 139 | ## 0.3.0 140 | 141 | - Check for duplicated `timing` define 142 | - Check for `arc` and `hold` items cut by `timing` event 143 | - Be more relax for endline token 144 | 145 | ## 0.2.0 146 | 147 | - Various simple file content checking 148 | - JSON validation for the `songlist` `packlist` `unlocks` files 149 | 150 | ## 0.1.1 151 | 152 | - Fix a bug in type checking of hold event 153 | 154 | ## 0.1.0 155 | 156 | - Parsing the AFF files and display basic syntax and semantic problems 157 | 158 | ## 0.0.1 159 | 160 | - Initial release 161 | - Syntax highlight 162 | - Useful Snippets -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 yojohanshinwataikei, and other vscode-arcaea-file-format maintainers 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vscode Arcaea File Format Support 2 | 3 | Util for reading and editing arcaea .aff files 4 | 5 | # Features and Roadmaps 6 | 7 | - [x] Basix Snippets 8 | - [x] Syntax highlight 9 | - [x] .aff File Format Parsing and Syntax Error Displaying 10 | - [ ] Tuning the error recovery heuristics 11 | - [ ] Use better customized error meassages 12 | - Semantic Problem Diagnostic, Displaying and fix 13 | - [x] Check value format for known metadatas 14 | - [x] Type assert and sub-events check for known events 15 | - [x] `timing` third param 16 | - [x] Track id of normal note 17 | - [x] `arctap` time out of `arc` 18 | - [ ] Fix: remove the `arctap` 19 | - [x] Negative length `arc` or not positive length `hold` 20 | - [ ] Fix: remove the `hold` or `arc` 21 | - [x] Zero length `arc` with non-`s` type 22 | - [ ] Fix: set type to `s` 23 | - [x] Zero length `arc` with `arctap` 24 | - [ ] Fix: remove the `arctap` 25 | - [x] Empty `arc` 26 | - [ ] Fix: remove the `arc` 27 | - [x] Duplicated `timing` 28 | - [x] Duplicated `arctap` 29 | - [ ] Fix: remove the `arctap` 30 | - [x] Wrong last param for `arc` with `arctap` 31 | - [ ] Fix: set it to correct value 32 | - [x] Out of range `arc` 33 | - [x] `arc` and `hold` across the `timing` 34 | - [x] Duplicated floor notes 35 | - [ ] Fix: merge the floor notes 36 | - [x] Duplicated `camera` 37 | - [ ] Simplifiable `arc` type 38 | - [ ] Fix: set it to most simple type 39 | - [ ] Listing more problems 40 | - Handful Editing Features 41 | - [ ] Resort 42 | - [ ] Mirroring 43 | - [ ] Move in time 44 | - [ ] Cut the `arc` 45 | - [ ] Align to timing 46 | - [ ] Listing more operations 47 | - JSON validation of the `songlist` `packlist` `unlocks` files 48 | - [x] `songlist` 49 | - [x] `packlist` 50 | - [x] `unlocks` -------------------------------------------------------------------------------- /client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-arcaea-file-format-client", 3 | "version": "0.14.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "vscode-arcaea-file-format-client", 9 | "version": "0.14.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "vscode-languageclient": "^8.0.2" 13 | }, 14 | "devDependencies": { 15 | "@types/vscode": "^1.55.0" 16 | }, 17 | "engines": { 18 | "vscode": "^1.70.0" 19 | } 20 | }, 21 | "node_modules/@types/vscode": { 22 | "version": "1.69.1", 23 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.69.1.tgz", 24 | "integrity": "sha512-YZ77g3u9S9Xw3dwAgRgNAwnKNS3nPlhSu3XKOIYQzCcItUrZovfJUlf/29wjON2VZvHGuYQnhKuJUP15ccpVIQ==", 25 | "dev": true 26 | }, 27 | "node_modules/balanced-match": { 28 | "version": "1.0.2", 29 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 30 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 31 | }, 32 | "node_modules/brace-expansion": { 33 | "version": "1.1.11", 34 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 35 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 36 | "dependencies": { 37 | "balanced-match": "^1.0.0", 38 | "concat-map": "0.0.1" 39 | } 40 | }, 41 | "node_modules/concat-map": { 42 | "version": "0.0.1", 43 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 44 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 45 | }, 46 | "node_modules/lru-cache": { 47 | "version": "6.0.0", 48 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 49 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 50 | "dependencies": { 51 | "yallist": "^4.0.0" 52 | }, 53 | "engines": { 54 | "node": ">=10" 55 | } 56 | }, 57 | "node_modules/minimatch": { 58 | "version": "3.1.2", 59 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 60 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 61 | "dependencies": { 62 | "brace-expansion": "^1.1.7" 63 | }, 64 | "engines": { 65 | "node": "*" 66 | } 67 | }, 68 | "node_modules/semver": { 69 | "version": "7.3.7", 70 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", 71 | "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", 72 | "dependencies": { 73 | "lru-cache": "^6.0.0" 74 | }, 75 | "bin": { 76 | "semver": "bin/semver.js" 77 | }, 78 | "engines": { 79 | "node": ">=10" 80 | } 81 | }, 82 | "node_modules/vscode-jsonrpc": { 83 | "version": "8.0.2", 84 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz", 85 | "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==", 86 | "engines": { 87 | "node": ">=14.0.0" 88 | } 89 | }, 90 | "node_modules/vscode-languageclient": { 91 | "version": "8.0.2", 92 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz", 93 | "integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==", 94 | "dependencies": { 95 | "minimatch": "^3.0.4", 96 | "semver": "^7.3.5", 97 | "vscode-languageserver-protocol": "3.17.2" 98 | }, 99 | "engines": { 100 | "vscode": "^1.67.0" 101 | } 102 | }, 103 | "node_modules/vscode-languageserver-protocol": { 104 | "version": "3.17.2", 105 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz", 106 | "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==", 107 | "dependencies": { 108 | "vscode-jsonrpc": "8.0.2", 109 | "vscode-languageserver-types": "3.17.2" 110 | } 111 | }, 112 | "node_modules/vscode-languageserver-types": { 113 | "version": "3.17.2", 114 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", 115 | "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" 116 | }, 117 | "node_modules/yallist": { 118 | "version": "4.0.0", 119 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 120 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 121 | } 122 | }, 123 | "dependencies": { 124 | "@types/vscode": { 125 | "version": "1.69.1", 126 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.69.1.tgz", 127 | "integrity": "sha512-YZ77g3u9S9Xw3dwAgRgNAwnKNS3nPlhSu3XKOIYQzCcItUrZovfJUlf/29wjON2VZvHGuYQnhKuJUP15ccpVIQ==", 128 | "dev": true 129 | }, 130 | "balanced-match": { 131 | "version": "1.0.2", 132 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 133 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 134 | }, 135 | "brace-expansion": { 136 | "version": "1.1.11", 137 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 138 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 139 | "requires": { 140 | "balanced-match": "^1.0.0", 141 | "concat-map": "0.0.1" 142 | } 143 | }, 144 | "concat-map": { 145 | "version": "0.0.1", 146 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 147 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 148 | }, 149 | "lru-cache": { 150 | "version": "6.0.0", 151 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 152 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 153 | "requires": { 154 | "yallist": "^4.0.0" 155 | } 156 | }, 157 | "minimatch": { 158 | "version": "3.1.2", 159 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 160 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 161 | "requires": { 162 | "brace-expansion": "^1.1.7" 163 | } 164 | }, 165 | "semver": { 166 | "version": "7.3.7", 167 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", 168 | "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", 169 | "requires": { 170 | "lru-cache": "^6.0.0" 171 | } 172 | }, 173 | "vscode-jsonrpc": { 174 | "version": "8.0.2", 175 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz", 176 | "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==" 177 | }, 178 | "vscode-languageclient": { 179 | "version": "8.0.2", 180 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz", 181 | "integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==", 182 | "requires": { 183 | "minimatch": "^3.0.4", 184 | "semver": "^7.3.5", 185 | "vscode-languageserver-protocol": "3.17.2" 186 | } 187 | }, 188 | "vscode-languageserver-protocol": { 189 | "version": "3.17.2", 190 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz", 191 | "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==", 192 | "requires": { 193 | "vscode-jsonrpc": "8.0.2", 194 | "vscode-languageserver-types": "3.17.2" 195 | } 196 | }, 197 | "vscode-languageserver-types": { 198 | "version": "3.17.2", 199 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", 200 | "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" 201 | }, 202 | "yallist": { 203 | "version": "4.0.0", 204 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 205 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-arcaea-file-format-client", 3 | "description": "Util for reading and editing arcaea .aff files, the vscode side", 4 | "license": "MIT", 5 | "version": "0.14.0", 6 | "repository": "github:yojohanshinwataikei/vscode-arcaea-file-format", 7 | "engines": { 8 | "vscode": "^1.70.0" 9 | }, 10 | "devDependencies": { 11 | "@types/vscode": "^1.55.0" 12 | }, 13 | "dependencies": { 14 | "vscode-languageclient": "^8.0.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path" 2 | import * as vscode from "vscode" 3 | import * as lsp from "vscode-languageclient/node" 4 | 5 | let client: lsp.LanguageClient = null 6 | 7 | export const activate = (context: vscode.ExtensionContext) => { 8 | let serverModule = context.asAbsolutePath(path.join("server", "out", "server.js")) 9 | let run: lsp.NodeModule = { 10 | module: serverModule, transport: lsp.TransportKind.ipc 11 | } 12 | let debug: lsp.NodeModule = { 13 | options: { 14 | execArgv: ["--nolazy", "--inspect=6009"] 15 | }, ...run 16 | } 17 | client = new lsp.LanguageClient( 18 | "arcaea-aff-lsp", 19 | { run, debug }, 20 | { documentSelector: [{ scheme: "file", language: "arcaea-aff" }] }, 21 | ) 22 | client.start() 23 | } 24 | 25 | export const deactivate = () => { 26 | if (client !== null) { 27 | return client.stop() 28 | } 29 | } -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es6", 6 | "outDir": "out", 7 | "rootDir": "src", 8 | "lib": ["es6"], 9 | "sourceMap": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", ".vscode-test"] 13 | } -------------------------------------------------------------------------------- /json-schema/packdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "https://raw.githubusercontent.com/yojohanshinwataikei/vscode-arcaea-file-format/master/json-schema/packdata.json", 4 | "title": "Arcaea pack metadata", 5 | "description": "The metadata of packs in Arcaea", 6 | "type": "object", 7 | "properties": { 8 | "id": { 9 | "type": "string", 10 | "description": "The id of the pack, used in the songlist file" 11 | }, 12 | "section": { 13 | "type": "string", 14 | "description": "The section of the pack, used in pack selection view", 15 | "enum": [ 16 | "archive", 17 | "free", 18 | "mainstory", 19 | "sidestory", 20 | "collab" 21 | ] 22 | }, 23 | "cost": { 24 | "type": "integer", 25 | "description": "The price of this pack in memories, however from 3.0.0 this will be controlled by server instead of keep in packlist", 26 | "minimum": 0, 27 | "deprecated": true 28 | }, 29 | "custom_banner": { 30 | "type": "boolean", 31 | "description": "Is a custom banner included in the cover image of the pack. If no, a banner will be automatically generated for this pack" 32 | }, 33 | "plus_character": { 34 | "type": "integer", 35 | "description": "The id of the associated partner of the pack, -1 if there are none associated partner", 36 | "minimum": -1 37 | }, 38 | "name_localized": { 39 | "description": "The name of the pack, localized in various languages", 40 | "$ref": "songdata.json#/definitions/localized_string" 41 | }, 42 | "description_localized": { 43 | "description": "The description of the pack used when the pack is not yet purchased, localized in various languages", 44 | "$ref": "songdata.json#/definitions/localized_string" 45 | } 46 | }, 47 | "required": [ 48 | "id", 49 | "name_localized", 50 | "section" 51 | ] 52 | } -------------------------------------------------------------------------------- /json-schema/packlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "https://raw.githubusercontent.com/yojohanshinwataikei/vscode-arcaea-file-format/master/json-schema/packlist.json", 4 | "title": "Arcaea packlist file", 5 | "description": "The file where Arcaea reads metadatas of packs", 6 | "type": "object", 7 | "properties": { 8 | "packs": { 9 | "description": "The list of metadata of packs", 10 | "type": "array", 11 | "items": { 12 | "$ref": "packdata.json" 13 | } 14 | } 15 | }, 16 | "required": [ 17 | "packs" 18 | ] 19 | } -------------------------------------------------------------------------------- /json-schema/songdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "https://raw.githubusercontent.com/yojohanshinwataikei/vscode-arcaea-file-format/master/json-schema/songdata.json", 4 | "title": "Arcaea song metadata", 5 | "description": "The metadata of songs in Arcaea", 6 | "type": "object", 7 | "properties": { 8 | "id": { 9 | "type": "string", 10 | "description": "The id of the song, used to find the folder containing the aff files and other resources" 11 | }, 12 | "idx": { 13 | "type": "number", 14 | "description": "The numeric id of the song, used in link play to determine the available charts" 15 | }, 16 | "deleted": { 17 | "type": "boolean", 18 | "description": "Is this song deleted" 19 | }, 20 | "title_localized": { 21 | "description": "The title of the song, localized in various languages", 22 | "$ref": "#/definitions/localized_string" 23 | }, 24 | "jacket_localized": { 25 | "description": "Should localized jacket be used in various languages", 26 | "patternProperties": { 27 | "en|ja|ko|zh-Hans|zh-Hant": { 28 | "description": "Should the localized assets be used", 29 | "type": "boolean" 30 | } 31 | } 32 | }, 33 | "artist": { 34 | "type": "string", 35 | "description": "The artist of the song" 36 | }, 37 | "artist_localized": { 38 | "description": "The artist of the song, in various languages. Overrides artist", 39 | "$ref": "#/definitions/localized_string" 40 | }, 41 | "search_title": { 42 | "description": "The strings used to search the song by title, in various languages", 43 | "$ref": "#/definitions/localized_search_string" 44 | }, 45 | "search_artist": { 46 | "description": "The strings used to search the song by artist, in various languages", 47 | "$ref": "#/definitions/localized_search_string" 48 | }, 49 | "bpm": { 50 | "type": "string", 51 | "description": "The bpm of the song, only used for displaying" 52 | }, 53 | "bpm_base": { 54 | "type": "number", 55 | "description": "The bpm used to calculating the note speed of the chart", 56 | "exclusiveMinimum": 0 57 | }, 58 | "set": { 59 | "type": "string", 60 | "description": "The id of the pack of the song specified in the packlist file" 61 | }, 62 | "purchase": { 63 | "type": "string", 64 | "description": "The id of purchased item that is needed to play this song, normally is the id of song or pack" 65 | }, 66 | "audioPreview": { 67 | "type": "integer", 68 | "description": "The start timestamp of song preview, in milliseconds" 69 | }, 70 | "audioPreviewEnd": { 71 | "type": "integer", 72 | "description": "The end timestamp of song preview, in milliseconds" 73 | }, 74 | "side": { 75 | "type": "integer", 76 | "description": "The side of the song, 0 for light, 1 for conflict, 2 for colorless, 3 for lephon", 77 | "enum": [ 78 | 0, 79 | 1, 80 | 2, 81 | 3 82 | ] 83 | }, 84 | "world_unlock": { 85 | "type": "boolean", 86 | "description": "Does playing the song require unlocking it in world mode" 87 | }, 88 | "byd_local_unlock": { 89 | "type": "boolean", 90 | "description": "Should the unlock state of beyond difficulty of the song saved locally (instead of saved online)" 91 | }, 92 | "songlist_hidden": { 93 | "type": "boolean", 94 | "description": "Should the song be hidden in the songlist before unlocking it" 95 | }, 96 | "bg": { 97 | "type": "string", 98 | "description": "The name of the background used when playing the song" 99 | }, 100 | "bg_inverse": { 101 | "type": "string", 102 | "description": "The name of the background used when playing the song with a partner which can inverse the side of the song" 103 | }, 104 | "bg_daynight": { 105 | "type": "object", 106 | "description": "The name of the background used when playing the song with a partner which can change the side of the song to match the current time", 107 | "properties": { 108 | "day": { 109 | "type": "string", 110 | "description": "The name of the background used when playing the song in the day" 111 | }, 112 | "night": { 113 | "type": "string", 114 | "description": "The name of the background used when playing the song in the night" 115 | } 116 | }, 117 | "required": [ 118 | "day", 119 | "night" 120 | ] 121 | }, 122 | "date": { 123 | "type": "integer", 124 | "description": "The Unix timestamp of the time when the song is added, used to sort the songs by time", 125 | "minimum": 0 126 | }, 127 | "version": { 128 | "type": "string", 129 | "description": "The version where the song was published, used to categorize songs by version" 130 | }, 131 | "remote_dl": { 132 | "type": "boolean", 133 | "description": "Is the assets of the song stored on the remote server" 134 | }, 135 | "source_localized": { 136 | "description": "The source of the song, localized in various languages", 137 | "$ref": "#/definitions/localized_string" 138 | }, 139 | "source_copyright": { 140 | "type": "string", 141 | "description": "The copyright information of the song" 142 | }, 143 | "no_stream": { 144 | "type": "boolean", 145 | "description": "Should the song be not playable in the streamer mode" 146 | }, 147 | "additional_files": { 148 | "type": "array", 149 | "description": "Additional files to be downloaded with the song", 150 | "items": { 151 | "oneOf": [ 152 | { 153 | "type": "string", 154 | "description": "The name of additional files" 155 | }, 156 | { 157 | "type": "object", 158 | "description": "The specification of additional files", 159 | "properties": { 160 | "file_name": { 161 | "type": "string", 162 | "description": "The name of additional files" 163 | }, 164 | "requirement": { 165 | "type": "string", 166 | "description": "The condition that this file is used", 167 | "enum": [ 168 | "low_res", 169 | "hi_res", 170 | "required" 171 | ] 172 | } 173 | }, 174 | "required": [ 175 | "file_name", 176 | "requirement" 177 | ] 178 | } 179 | ] 180 | } 181 | }, 182 | "difficulties": { 183 | "type": "array", 184 | "description": "The difficulties of the song", 185 | "items": { 186 | "type": "object", 187 | "description": "A difficulty of the song", 188 | "properties": { 189 | "ratingClass": { 190 | "type": "integer", 191 | "description": "The rating class of the difficulty, 0 for past, 1 for present, 2 for future, 3 for beyond, and 4 for eternal", 192 | "enum": [ 193 | 0, 194 | 1, 195 | 2, 196 | 3, 197 | 4 198 | ] 199 | }, 200 | "chartDesigner": { 201 | "type": "string", 202 | "description": "The designer of the chart of the difficulty" 203 | }, 204 | "jacketDesigner": { 205 | "type": "string", 206 | "description": "The designer of the jacket of the difficulty" 207 | }, 208 | "rating": { 209 | "type": "integer", 210 | "minimum": 0, 211 | "description": "The rating of the difficulty, 0 for \"?\", other number for corresponding number value. before 3.0.0 it is 0 for \"?\", 10 for \"9+\", 11 for \"10\" and 1~9 for corresponding number value" 212 | }, 213 | "ratingPlus": { 214 | "type": "boolean", 215 | "description": "Should the difficulty of the chart annotated with a plus sign, like 9+, 10+" 216 | }, 217 | "legacy11": { 218 | "type": "boolean", 219 | "description": "Should the chart use a harder gauge, like all the chart with 11 or higher difficulties before 6.0.0" 220 | }, 221 | "plusFingers": { 222 | "description": "Should this charts be played with more than two fingers. This field is not longer used since the judging code can normally handle multiple fingers now", 223 | "oneOf": [ 224 | { 225 | "type": "boolean" 226 | }, 227 | { 228 | "type": "integer", 229 | "enum": [ 230 | 0, 231 | 1 232 | ] 233 | } 234 | ], 235 | "deprecated": true 236 | }, 237 | "jacketOverride": { 238 | "type": "boolean", 239 | "description": "Is a different jacket used for the difficulty" 240 | }, 241 | "title_localized": { 242 | "description": "The title of the song in this difficulty, localized in various languages", 243 | "$ref": "#/definitions/localized_string" 244 | }, 245 | "artist": { 246 | "type": "string", 247 | "description": "The artist of the song in this difficulty" 248 | }, 249 | "artist_localized": { 250 | "description": "The artist of the song in this difficulty, in various languages. Overrides artist", 251 | "$ref": "#/definitions/localized_string" 252 | }, 253 | "audioOverride": { 254 | "type": "boolean", 255 | "description": "Is a different audio used for the difficulty" 256 | }, 257 | "jacket_night": { 258 | "type": "string", 259 | "description": "The name of the jacket used in the night in the difficulty" 260 | }, 261 | "hidden_until_unlocked": { 262 | "type": "boolean", 263 | "description": "Should this chart be hidden in the songlist before unlocking it" 264 | }, 265 | "hidden_until": { 266 | "type": "string", 267 | "description": "The condition to reveal the hidden song, none for always visible, always for always hidden, difficulty for unlock of the difficulty, song for unlock of the song", 268 | "enum": [ 269 | "none", 270 | "always", 271 | "difficulty", 272 | "song" 273 | ] 274 | }, 275 | "world_unlock": { 276 | "type": "boolean", 277 | "description": "Does playing the difficulty requires unlocking it in world mode" 278 | }, 279 | "bg": { 280 | "type": "string", 281 | "description": "The name of the background used for this difficulty when playing the song, override the bg of the song" 282 | }, 283 | "bg_inverse": { 284 | "type": "string", 285 | "description": "The name of the background used when playing the song with a partner which can inverse the side of the song" 286 | }, 287 | "bpm": { 288 | "type": "string", 289 | "description": "The bpm of the this difficulty, only used for displaying, override the bpm of the song" 290 | }, 291 | "bpm_base": { 292 | "type": "number", 293 | "description": "The bpm used to calculating the note speed of the chart in this difficulty, override the bpm_base of the song", 294 | "exclusiveMinimum": 0 295 | }, 296 | "version": { 297 | "type": "string", 298 | "description": "The version where this difficulty was published, used to categorize songs by version, override the version of the song" 299 | }, 300 | "date": { 301 | "type": "integer", 302 | "description": "The Unix timestamp of the time when this difficulty is added, used to sort the songs by time, override the date of the song", 303 | "minimum": 0 304 | } 305 | }, 306 | "required": [ 307 | "chartDesigner", 308 | "jacketDesigner", 309 | "ratingClass", 310 | "rating" 311 | ] 312 | } 313 | } 314 | }, 315 | "required": [ 316 | "id" 317 | ], 318 | "if": { 319 | "properties": { 320 | "deleted": { 321 | "const": true 322 | } 323 | } 324 | }, 325 | "then": true, 326 | "else": { 327 | "required": [ 328 | "title_localized", 329 | "artist", 330 | "bpm", 331 | "bpm_base", 332 | "set", 333 | "purchase", 334 | "audioPreview", 335 | "audioPreviewEnd", 336 | "side", 337 | "version", 338 | "date" 339 | ] 340 | }, 341 | "definitions": { 342 | "localized_string": { 343 | "$comment": "The schema is used as a schema for localized string", 344 | "type": "object", 345 | "patternProperties": { 346 | "en|ja|ko|zh-Hans|zh-Hant": { 347 | "description": "The localized string", 348 | "type": "string" 349 | } 350 | }, 351 | "required": [ 352 | "en" 353 | ] 354 | }, 355 | "localized_search_string": { 356 | "$comment": "The schema is used as a schema for localized strings used for search", 357 | "type": "object", 358 | "patternProperties": { 359 | "en|ja|ko|zh-Hans|zh-Hant": { 360 | "description": "The localized strings used for search", 361 | "type": "array", 362 | "items": { 363 | "type": "string" 364 | } 365 | } 366 | } 367 | } 368 | } 369 | } -------------------------------------------------------------------------------- /json-schema/songlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "https://raw.githubusercontent.com/yojohanshinwataikei/vscode-arcaea-file-format/master/json-schema/songlist.json", 4 | "title": "Arcaea songlist file", 5 | "description": "The file where Arcaea reads metadatas of songs", 6 | "type": "object", 7 | "properties": { 8 | "songs": { 9 | "description": "The list of metadata of songs", 10 | "type": "array", 11 | "items": { 12 | "$ref": "songdata.json" 13 | } 14 | } 15 | }, 16 | "required": [ 17 | "songs" 18 | ] 19 | } -------------------------------------------------------------------------------- /json-schema/unlockdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "https://raw.githubusercontent.com/yojohanshinwataikei/vscode-arcaea-file-format/master/json-schema/packdata.json", 4 | "title": "Arcaea unlock condition", 5 | "description": "The unlock condition of songs in Arcaea", 6 | "type": "object", 7 | "properties": { 8 | "songId": { 9 | "type": "string", 10 | "description": "The id of the song needed to unlock specified in songlist file" 11 | }, 12 | "ratingClass": { 13 | "type": "integer", 14 | "description": "The rating class of the difficulty that needs to be unlocked, 0 for past, 1 for present 2 for future, 3 for beyond, and 4 for eternal", 15 | "enum": [ 16 | 0, 17 | 1, 18 | 2, 19 | 3, 20 | 4 21 | ] 22 | }, 23 | "conditions": { 24 | "type": "array", 25 | "description": "The unlock conditions of the difficulty", 26 | "items": { 27 | "$ref": "#/definitions/condition" 28 | } 29 | } 30 | }, 31 | "required": [ 32 | "songId", 33 | "ratingClass", 34 | "conditions" 35 | ], 36 | "definitions": { 37 | "condition": { 38 | "description": "A unlock condition", 39 | "type": "object", 40 | "properties": { 41 | "type": { 42 | "type": "integer", 43 | "description": "The type of the unlock condition, 0 for fragments, 1 for grade on earlier charts, 2 for play on earlier charts, 3 for multiple grade on earlier charts, 4 for multiple selectable conditions, 5 for reaching a potential rating, 6 for clear of chart of specific rating, 7 for unlock of earlier charts, 8 for lamp on earlier charts, 9 for grade on chart of specific difficulty, 10 for story read, 101 for anomaly, 103 for a specific partner, 104 for online check in axiom of the end, 105 for using a specific partner in a specific state or not, 106 for unlocking another chart or not, 107 for selected songs with specific first letter, 108 for specific story read, 109 for puzzling play in specific rating class, 110 for special unlocking course, 111 for specific story read, 112 for puzzling song selecting" 44 | } 45 | }, 46 | "oneOf": [ 47 | { 48 | "properties": { 49 | "type": { 50 | "const": 0 51 | }, 52 | "credit": { 53 | "type": "integer", 54 | "description": "The fragments needed to unlock the song" 55 | } 56 | }, 57 | "required": [ 58 | "credit" 59 | ] 60 | }, 61 | { 62 | "properties": { 63 | "type": { 64 | "const": 1 65 | }, 66 | "song_id": { 67 | "type": "string", 68 | "description": "The id of the song needed to clear specified in songlist file" 69 | }, 70 | "song_difficulty": { 71 | "type": "integer", 72 | "description": "The rating class of the difficulty that needs to clear, 0 for past, 1 for present, 2 for future, 3 for beyond, and 4 for eternal", 73 | "enum": [ 74 | 0, 75 | 1, 76 | 2, 77 | 3, 78 | 4 79 | ] 80 | }, 81 | "grade": { 82 | "type": "integer", 83 | "description": "The require grade to the charts when cleared, 0 for no limit, 1 for C, 2 for B, 3 for A, 4 for AA, 5 for EX, 6 for EX+", 84 | "enum": [ 85 | 0, 86 | 1, 87 | 2, 88 | 3, 89 | 4, 90 | 5, 91 | 6 92 | ] 93 | } 94 | }, 95 | "required": [ 96 | "song_id", 97 | "song_difficulty", 98 | "grade" 99 | ] 100 | }, 101 | { 102 | "properties": { 103 | "type": { 104 | "const": 2 105 | }, 106 | "song_id": { 107 | "type": "string", 108 | "description": "The id of the song needed to play specified in songlist file" 109 | }, 110 | "song_difficulty": { 111 | "type": "integer", 112 | "description": "The rating class of the difficulty that needs to clear, 0 for past, 1 for present, 2 for future, 3 for beyond, and 4 for eternal", 113 | "enum": [ 114 | 0, 115 | 1, 116 | 2, 117 | 3, 118 | 4 119 | ] 120 | } 121 | }, 122 | "required": [ 123 | "song_id", 124 | "song_difficulty" 125 | ] 126 | }, 127 | { 128 | "properties": { 129 | "type": { 130 | "const": 3 131 | }, 132 | "song_id": { 133 | "type": "string", 134 | "description": "The id of the song needed to clear specified in songlist file" 135 | }, 136 | "song_difficulty": { 137 | "type": "integer", 138 | "description": "The rating class of the difficulty that needs to clear, 0 for past, 1 for present, 2 for future, 3 for beyond, and 4 for eternal", 139 | "enum": [ 140 | 0, 141 | 1, 142 | 2, 143 | 3, 144 | 4 145 | ] 146 | }, 147 | "grade": { 148 | "type": "integer", 149 | "description": "The require grade to the charts when cleared, 0 for no limit, 1 for C, 2 for B, 3 for A, 4 for AA, 5 for EX, 6 for EX+", 150 | "enum": [ 151 | 0, 152 | 1, 153 | 2, 154 | 3, 155 | 4, 156 | 5, 157 | 6 158 | ] 159 | }, 160 | "times": { 161 | "type": "integer", 162 | "description": "The required times of clear of the song with the specified grade", 163 | "minimum": 1 164 | } 165 | }, 166 | "required": [ 167 | "song_id", 168 | "song_difficulty", 169 | "grade", 170 | "times" 171 | ] 172 | }, 173 | { 174 | "properties": { 175 | "type": { 176 | "const": 4 177 | }, 178 | "conditions": { 179 | "type": "array", 180 | "description": "The available options of selectable conditions", 181 | "items": { 182 | "$ref": "#/definitions/condition" 183 | } 184 | } 185 | }, 186 | "required": [ 187 | "conditions" 188 | ] 189 | }, 190 | { 191 | "properties": { 192 | "type": { 193 | "const": 5 194 | }, 195 | "rating": { 196 | "type": "integer", 197 | "description": "The required rating needed to unlock the song, 100 times of potential value" 198 | } 199 | }, 200 | "required": [ 201 | "rating" 202 | ] 203 | }, 204 | { 205 | "properties": { 206 | "type": { 207 | "const": 6 208 | }, 209 | "count": { 210 | "type": "integer", 211 | "minimum": 0, 212 | "description": "Number of charts of the rating required to be clear" 213 | }, 214 | "rating": { 215 | "type": "integer", 216 | "minimum": 0, 217 | "description": "The rating of the difficulty, 0 for \"?\", other number for corresponding number value. before 3.0.0 it is 0 for \"?\", 10 for \"9+\", 11 for \"10\" and 1~9 for corresponding number value" 218 | }, 219 | "ratingPlus": { 220 | "type": "boolean", 221 | "description": "Should the difficulty of the chart annotated with a plus sign, like 9+, 10+" 222 | } 223 | }, 224 | "required": [ 225 | "count", 226 | "rating" 227 | ] 228 | }, 229 | { 230 | "properties": { 231 | "type": { 232 | "const": 7 233 | }, 234 | "song_id": { 235 | "type": "string", 236 | "description": "The id of the song needed to unlock specified in songlist file" 237 | }, 238 | "song_difficulty": { 239 | "type": "integer", 240 | "description": "The rating class of the difficulty that needs to unlock, 0 for past, 1 for present, 2 for future, 3 for beyond, and 4 for eternal", 241 | "enum": [ 242 | 0, 243 | 1, 244 | 2, 245 | 3, 246 | 4 247 | ] 248 | } 249 | }, 250 | "required": [ 251 | "song_id", 252 | "song_difficulty" 253 | ] 254 | }, 255 | { 256 | "properties": { 257 | "type": { 258 | "const": 8 259 | }, 260 | "song_id": { 261 | "type": "string", 262 | "description": "The id of the song needed to unlock specified in songlist file" 263 | }, 264 | "song_difficulty": { 265 | "type": "integer", 266 | "description": "The rating class of the difficulty that needs to unlock, 0 for past, 1 for present, 2 for future, 3 for beyond, and 4 for eternal", 267 | "enum": [ 268 | 0, 269 | 1, 270 | 2, 271 | 3, 272 | 4 273 | ] 274 | }, 275 | "lamp": { 276 | "type": "integer", 277 | "description": "The lamp that needs to unlock, 0 for track lost, 1 for normal clear, 2 for full recall, 3 for pure memory, 4 for easy clear, and 5 for hard clear", 278 | "enum": [ 279 | 0, 280 | 1, 281 | 2, 282 | 3, 283 | 4, 284 | 5 285 | ] 286 | } 287 | }, 288 | "required": [ 289 | "song_id", 290 | "song_difficulty", 291 | "lamp" 292 | ] 293 | }, 294 | { 295 | "properties": { 296 | "type": { 297 | "const": 9 298 | }, 299 | "count": { 300 | "type": "integer", 301 | "minimum": 0, 302 | "description": "Number of charts of the rating required to be clear" 303 | }, 304 | "difficulty": { 305 | "type": "integer", 306 | "description": "The rating class of the difficulty that needs to unlock, 0 for past, 1 for present, 2 for future, 3 for beyond, and 4 for eternal", 307 | "enum": [ 308 | 0, 309 | 1, 310 | 2, 311 | 3, 312 | 4 313 | ] 314 | }, 315 | "grade": { 316 | "type": "integer", 317 | "description": "The require grade to the charts when cleared, 0 for no limit, 1 for C, 2 for B, 3 for A, 4 for AA, 5 for EX, 6 for EX+", 318 | "enum": [ 319 | 0, 320 | 1, 321 | 2, 322 | 3, 323 | 4, 324 | 5, 325 | 6 326 | ] 327 | } 328 | }, 329 | "required": [ 330 | "count", 331 | "difficulty", 332 | "grade" 333 | ] 334 | }, 335 | { 336 | "properties": { 337 | "type": { 338 | "const": 10 339 | }, 340 | "major": { 341 | "type": "integer", 342 | "minimum": 0, 343 | "description": "The number id of the story series" 344 | }, 345 | "minor": { 346 | "type": "integer", 347 | "minimum": 0, 348 | "description": "The number id of the story entry in the series" 349 | } 350 | }, 351 | "required": [ 352 | "major", 353 | "minor" 354 | ] 355 | }, 356 | { 357 | "properties": { 358 | "type": { 359 | "const": 101 360 | }, 361 | "min": { 362 | "type": "integer", 363 | "description": "The minimum progress when failed to unlock this song", 364 | "minimum": 0, 365 | "maximum": 100 366 | }, 367 | "max": { 368 | "type": "integer", 369 | "description": "The maximum progress when failed to unlock this song", 370 | "minimum": 0, 371 | "maximum": 100 372 | } 373 | }, 374 | "required": [ 375 | "min", 376 | "max" 377 | ] 378 | }, 379 | { 380 | "properties": { 381 | "type": { 382 | "const": 103 383 | }, 384 | "id": { 385 | "type": "integer", 386 | "description": "The id of the partner needed to unlock this song" 387 | } 388 | }, 389 | "required": [ 390 | "id" 391 | ] 392 | }, 393 | { 394 | "properties": { 395 | "type": { 396 | "const": 104 397 | } 398 | } 399 | }, 400 | { 401 | "properties": { 402 | "type": { 403 | "const": 105 404 | }, 405 | "char_id": { 406 | "type": "integer", 407 | "description": "The id of the partner in the unlock condition" 408 | }, 409 | "awakened": { 410 | "type": "boolean", 411 | "description": "Should the partner be awakened" 412 | }, 413 | "inverted": { 414 | "type": "boolean", 415 | "description": "Is the unlock condition inverted" 416 | } 417 | }, 418 | "required": [ 419 | "char_id", 420 | "awakened", 421 | "inverted" 422 | ] 423 | }, 424 | { 425 | "properties": { 426 | "type": { 427 | "const": 106 428 | }, 429 | "song_id": { 430 | "type": "string", 431 | "description": "The id of the song in the unlock condition" 432 | }, 433 | "song_difficulty": { 434 | "type": "integer", 435 | "description": "The rating class of the difficulty in the unlock condition, 0 for past, 1 for present, 2 for future, 3 for beyond, and 4 for eternal", 436 | "enum": [ 437 | 0, 438 | 1, 439 | 2, 440 | 3, 441 | 4 442 | ] 443 | }, 444 | "inverted": { 445 | "type": "boolean", 446 | "description": "Is the unlock condition inverted" 447 | } 448 | }, 449 | "required": [ 450 | "song_id", 451 | "song_difficulty", 452 | "inverted" 453 | ] 454 | }, 455 | { 456 | "properties": { 457 | "type": { 458 | "const": 107 459 | } 460 | } 461 | }, 462 | { 463 | "properties": { 464 | "type": { 465 | "const": 108 466 | } 467 | } 468 | }, 469 | { 470 | "properties": { 471 | "type": { 472 | "const": 109 473 | }, 474 | "index": { 475 | "type": "integer", 476 | "description": "The type of the puzzle", 477 | "enum": [ 478 | 0, 479 | 1, 480 | 2 481 | ] 482 | }, 483 | "difficulty": { 484 | "type": "integer", 485 | "description": "The rating class of the difficulty in the unlock condition, 0 for past, 1 for present, 2 for future, 3 for beyond, and 4 for eternal", 486 | "enum": [ 487 | 0, 488 | 1, 489 | 2, 490 | 3, 491 | 4 492 | ] 493 | } 494 | }, 495 | "required": [ 496 | "index", 497 | "difficulty" 498 | ] 499 | }, 500 | { 501 | "properties": { 502 | "type": { 503 | "const": 110 504 | } 505 | } 506 | }, 507 | { 508 | "properties": { 509 | "type": { 510 | "const": 111 511 | } 512 | } 513 | }, 514 | { 515 | "properties": { 516 | "type": { 517 | "const": 112 518 | } 519 | } 520 | } 521 | ], 522 | "required": [ 523 | "type" 524 | ] 525 | } 526 | } 527 | } -------------------------------------------------------------------------------- /json-schema/unlocks.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "https://raw.githubusercontent.com/yojohanshinwataikei/vscode-arcaea-file-format/master/json-schema/unlocks.json", 4 | "title": "Arcaea unlocks file", 5 | "description": "The file where Arcaea reads unlock conditions of songs", 6 | "type": "object", 7 | "properties": { 8 | "unlocks": { 9 | "description": "The list of conditions of songs", 10 | "type": "array", 11 | "items": { 12 | "$ref": "unlockdata.json" 13 | } 14 | } 15 | }, 16 | "required": [ 17 | "unlocks" 18 | ] 19 | } -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": {}, 3 | "brackets": [ 4 | ["[", "]"], 5 | ["(", ")"] 6 | ], 7 | "autoClosingPairs": [ 8 | ["[", "]"], 9 | ["(", ")"] 10 | ], 11 | "surroundingPairs": [ 12 | ["[", "]"], 13 | ["(", ")"] 14 | ] 15 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-arcaea-file-format", 3 | "version": "0.14.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "vscode-arcaea-file-format", 9 | "version": "0.14.0", 10 | "hasInstallScript": true, 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@types/node": "^16.11.7", 14 | "typescript": "^4.7.3" 15 | }, 16 | "engines": { 17 | "vscode": "^1.70.0" 18 | } 19 | }, 20 | "node_modules/@types/node": { 21 | "version": "16.11.47", 22 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", 23 | "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", 24 | "dev": true 25 | }, 26 | "node_modules/typescript": { 27 | "version": "4.7.4", 28 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", 29 | "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", 30 | "dev": true, 31 | "bin": { 32 | "tsc": "bin/tsc", 33 | "tsserver": "bin/tsserver" 34 | }, 35 | "engines": { 36 | "node": ">=4.2.0" 37 | } 38 | } 39 | }, 40 | "dependencies": { 41 | "@types/node": { 42 | "version": "16.11.47", 43 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", 44 | "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", 45 | "dev": true 46 | }, 47 | "typescript": { 48 | "version": "4.7.4", 49 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", 50 | "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", 51 | "dev": true 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-arcaea-file-format", 3 | "displayName": "Vscode Arcaea File Format Support", 4 | "description": "Util for reading and editing arcaea .aff files", 5 | "license": "MIT", 6 | "version": "0.14.0", 7 | "publisher": "yojohanshinwataikei", 8 | "repository": "github:yojohanshinwataikei/vscode-arcaea-file-format", 9 | "engines": { 10 | "vscode": "^1.70.0" 11 | }, 12 | "categories": [ 13 | "Other" 14 | ], 15 | "activationEvents": [ 16 | "onLanguage:arcaea-aff" 17 | ], 18 | "main": "./client/out/extension", 19 | "contributes": { 20 | "languages": [ 21 | { 22 | "id": "arcaea-aff", 23 | "aliases": [ 24 | "Arcaea File Format", 25 | "arcaea-aff" 26 | ], 27 | "extensions": [ 28 | ".aff" 29 | ], 30 | "configuration": "./language-configuration.json" 31 | }, 32 | { 33 | "id": "json", 34 | "filenames": [ 35 | "songlist", 36 | "packlist", 37 | "unlocks" 38 | ] 39 | } 40 | ], 41 | "grammars": [ 42 | { 43 | "language": "arcaea-aff", 44 | "scopeName": "source.arcaea-aff", 45 | "path": "./syntaxes/arcaea-aff.tmLanguage.json" 46 | } 47 | ], 48 | "snippets": [ 49 | { 50 | "language": "arcaea-aff", 51 | "path": "./snippets.json" 52 | } 53 | ], 54 | "jsonValidation": [ 55 | { 56 | "fileMatch": "songlist", 57 | "url": "./json-schema/songlist.json" 58 | }, 59 | { 60 | "fileMatch": "songlist.json", 61 | "url": "./json-schema/songlist.json" 62 | }, 63 | { 64 | "fileMatch": "songdata.json", 65 | "url": "./json-schema/songdata.json" 66 | }, 67 | { 68 | "fileMatch": "packlist", 69 | "url": "./json-schema/packlist.json" 70 | }, 71 | { 72 | "fileMatch": "packlist.json", 73 | "url": "./json-schema/packlist.json" 74 | }, 75 | { 76 | "fileMatch": "packdata.json", 77 | "url": "./json-schema/packdata.json" 78 | }, 79 | { 80 | "fileMatch": "unlocks", 81 | "url": "./json-schema/unlocks.json" 82 | }, 83 | { 84 | "fileMatch": "unlocks.json", 85 | "url": "./json-schema/unlocks.json" 86 | }, 87 | { 88 | "fileMatch": "unlockdata.json", 89 | "url": "./json-schema/unlockdata.json" 90 | } 91 | ], 92 | "configuration": [ 93 | { 94 | "title": "Arcaea File Format", 95 | "properties": { 96 | "arcaeaFileFormat.diagnosticLevel": { 97 | "description": "Specify the minimal level of reported diagnostic", 98 | "type": "string", 99 | "default": "all", 100 | "enum": [ 101 | "all", 102 | "warn", 103 | "error" 104 | ] 105 | } 106 | } 107 | } 108 | ] 109 | }, 110 | "scripts": { 111 | "vscode:prepublish": "npm run compile", 112 | "compile": "tsc -b", 113 | "watch": "tsc -b -w", 114 | "postinstall": "cd client && npm install && cd ../server && npm install && cd .." 115 | }, 116 | "devDependencies": { 117 | "@types/node": "^16.11.7", 118 | "typescript": "^4.7.3" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arcaea-file-format-server", 3 | "version": "0.14.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "arcaea-file-format-server", 9 | "version": "0.14.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "chevrotain": "^10.1.2", 13 | "vscode-languageserver": "^8.0.2", 14 | "vscode-languageserver-textdocument": "^1.0.5" 15 | }, 16 | "engines": { 17 | "node": "*" 18 | } 19 | }, 20 | "node_modules/@chevrotain/cst-dts-gen": { 21 | "version": "10.1.2", 22 | "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.1.2.tgz", 23 | "integrity": "sha512-E/XrL0QlzExycPzwhOEZGVOheJ/Clr5uNv3oCds88MiNqEmg3UU1iauZk7DhjsUo3jgEW4lf0I5HRl7/HC5ZkQ==", 24 | "dependencies": { 25 | "@chevrotain/gast": "^10.1.2", 26 | "@chevrotain/types": "^10.1.2", 27 | "lodash": "4.17.21" 28 | } 29 | }, 30 | "node_modules/@chevrotain/gast": { 31 | "version": "10.1.2", 32 | "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.1.2.tgz", 33 | "integrity": "sha512-er+TcxUOMuGOPoiOq8CJsRm92zGE4YPIYtyxJfxoVwVgtj4AMrPNCmrHvYaK/bsbt2DaDuFdcbbAfM9bcBXW6Q==", 34 | "dependencies": { 35 | "@chevrotain/types": "^10.1.2", 36 | "lodash": "4.17.21" 37 | } 38 | }, 39 | "node_modules/@chevrotain/types": { 40 | "version": "10.1.2", 41 | "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.1.2.tgz", 42 | "integrity": "sha512-4qF9SmmWKv8AIG/3d+71VFuqLumNCQTP5GoL0CW6x7Ay2OdXm6FUgWFLTMneGUjYUk2C+MSCf7etQfdq3LEr1A==" 43 | }, 44 | "node_modules/@chevrotain/utils": { 45 | "version": "10.1.2", 46 | "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.1.2.tgz", 47 | "integrity": "sha512-bbZIpW6fdyf7FMaeDmw3cBbkTqsecxEkwlVKgVfqqXWBPLH6azxhPA2V9F7OhoZSVrsnMYw7QuyK6qutXPjEew==" 48 | }, 49 | "node_modules/chevrotain": { 50 | "version": "10.1.2", 51 | "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.1.2.tgz", 52 | "integrity": "sha512-hvRiQuhhTZxkPMGD/dke+s1EGo8AkKDBU05CcufBO278qgAQSwIC4QyLdHz0CFHVtqVYWjlAS5D1KwvBbaHT+w==", 53 | "dependencies": { 54 | "@chevrotain/cst-dts-gen": "^10.1.2", 55 | "@chevrotain/gast": "^10.1.2", 56 | "@chevrotain/types": "^10.1.2", 57 | "@chevrotain/utils": "^10.1.2", 58 | "lodash": "4.17.21", 59 | "regexp-to-ast": "0.5.0" 60 | } 61 | }, 62 | "node_modules/lodash": { 63 | "version": "4.17.21", 64 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 65 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 66 | }, 67 | "node_modules/regexp-to-ast": { 68 | "version": "0.5.0", 69 | "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", 70 | "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" 71 | }, 72 | "node_modules/vscode-jsonrpc": { 73 | "version": "8.0.2", 74 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz", 75 | "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==", 76 | "engines": { 77 | "node": ">=14.0.0" 78 | } 79 | }, 80 | "node_modules/vscode-languageserver": { 81 | "version": "8.0.2", 82 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.0.2.tgz", 83 | "integrity": "sha512-bpEt2ggPxKzsAOZlXmCJ50bV7VrxwCS5BI4+egUmure/oI/t4OlFzi/YNtVvY24A2UDOZAgwFGgnZPwqSJubkA==", 84 | "dependencies": { 85 | "vscode-languageserver-protocol": "3.17.2" 86 | }, 87 | "bin": { 88 | "installServerIntoExtension": "bin/installServerIntoExtension" 89 | } 90 | }, 91 | "node_modules/vscode-languageserver-protocol": { 92 | "version": "3.17.2", 93 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz", 94 | "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==", 95 | "dependencies": { 96 | "vscode-jsonrpc": "8.0.2", 97 | "vscode-languageserver-types": "3.17.2" 98 | } 99 | }, 100 | "node_modules/vscode-languageserver-textdocument": { 101 | "version": "1.0.5", 102 | "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz", 103 | "integrity": "sha512-1ah7zyQjKBudnMiHbZmxz5bYNM9KKZYz+5VQLj+yr8l+9w3g+WAhCkUkWbhMEdC5u0ub4Ndiye/fDyS8ghIKQg==" 104 | }, 105 | "node_modules/vscode-languageserver-types": { 106 | "version": "3.17.2", 107 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", 108 | "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" 109 | } 110 | }, 111 | "dependencies": { 112 | "@chevrotain/cst-dts-gen": { 113 | "version": "10.1.2", 114 | "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.1.2.tgz", 115 | "integrity": "sha512-E/XrL0QlzExycPzwhOEZGVOheJ/Clr5uNv3oCds88MiNqEmg3UU1iauZk7DhjsUo3jgEW4lf0I5HRl7/HC5ZkQ==", 116 | "requires": { 117 | "@chevrotain/gast": "^10.1.2", 118 | "@chevrotain/types": "^10.1.2", 119 | "lodash": "4.17.21" 120 | } 121 | }, 122 | "@chevrotain/gast": { 123 | "version": "10.1.2", 124 | "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.1.2.tgz", 125 | "integrity": "sha512-er+TcxUOMuGOPoiOq8CJsRm92zGE4YPIYtyxJfxoVwVgtj4AMrPNCmrHvYaK/bsbt2DaDuFdcbbAfM9bcBXW6Q==", 126 | "requires": { 127 | "@chevrotain/types": "^10.1.2", 128 | "lodash": "4.17.21" 129 | } 130 | }, 131 | "@chevrotain/types": { 132 | "version": "10.1.2", 133 | "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.1.2.tgz", 134 | "integrity": "sha512-4qF9SmmWKv8AIG/3d+71VFuqLumNCQTP5GoL0CW6x7Ay2OdXm6FUgWFLTMneGUjYUk2C+MSCf7etQfdq3LEr1A==" 135 | }, 136 | "@chevrotain/utils": { 137 | "version": "10.1.2", 138 | "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.1.2.tgz", 139 | "integrity": "sha512-bbZIpW6fdyf7FMaeDmw3cBbkTqsecxEkwlVKgVfqqXWBPLH6azxhPA2V9F7OhoZSVrsnMYw7QuyK6qutXPjEew==" 140 | }, 141 | "chevrotain": { 142 | "version": "10.1.2", 143 | "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.1.2.tgz", 144 | "integrity": "sha512-hvRiQuhhTZxkPMGD/dke+s1EGo8AkKDBU05CcufBO278qgAQSwIC4QyLdHz0CFHVtqVYWjlAS5D1KwvBbaHT+w==", 145 | "requires": { 146 | "@chevrotain/cst-dts-gen": "^10.1.2", 147 | "@chevrotain/gast": "^10.1.2", 148 | "@chevrotain/types": "^10.1.2", 149 | "@chevrotain/utils": "^10.1.2", 150 | "lodash": "4.17.21", 151 | "regexp-to-ast": "0.5.0" 152 | } 153 | }, 154 | "lodash": { 155 | "version": "4.17.21", 156 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 157 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 158 | }, 159 | "regexp-to-ast": { 160 | "version": "0.5.0", 161 | "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", 162 | "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" 163 | }, 164 | "vscode-jsonrpc": { 165 | "version": "8.0.2", 166 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz", 167 | "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==" 168 | }, 169 | "vscode-languageserver": { 170 | "version": "8.0.2", 171 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-8.0.2.tgz", 172 | "integrity": "sha512-bpEt2ggPxKzsAOZlXmCJ50bV7VrxwCS5BI4+egUmure/oI/t4OlFzi/YNtVvY24A2UDOZAgwFGgnZPwqSJubkA==", 173 | "requires": { 174 | "vscode-languageserver-protocol": "3.17.2" 175 | } 176 | }, 177 | "vscode-languageserver-protocol": { 178 | "version": "3.17.2", 179 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz", 180 | "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==", 181 | "requires": { 182 | "vscode-jsonrpc": "8.0.2", 183 | "vscode-languageserver-types": "3.17.2" 184 | } 185 | }, 186 | "vscode-languageserver-textdocument": { 187 | "version": "1.0.5", 188 | "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.5.tgz", 189 | "integrity": "sha512-1ah7zyQjKBudnMiHbZmxz5bYNM9KKZYz+5VQLj+yr8l+9w3g+WAhCkUkWbhMEdC5u0ub4Ndiye/fDyS8ghIKQg==" 190 | }, 191 | "vscode-languageserver-types": { 192 | "version": "3.17.2", 193 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", 194 | "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arcaea-file-format-server", 3 | "description": "Util for reading and editing arcaea .aff files, the LSP side", 4 | "license": "MIT", 5 | "version": "0.14.0", 6 | "repository": "github:yojohanshinwataikei/vscode-arcaea-file-format", 7 | "engines": { 8 | "node": "*" 9 | }, 10 | "dependencies": { 11 | "chevrotain": "^10.1.2", 12 | "vscode-languageserver": "^8.0.2", 13 | "vscode-languageserver-textdocument": "^1.0.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/associated-data/allow-memes.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AFFError, AFFFile, WithLocation, AFFItem, isLine } from "../types"; 3 | import { AssociatedDataMap } from "../util/associated-data"; 4 | import { DiagnosticSeverity } from "vscode-languageserver"; 5 | import { timings } from "./timing"; 6 | 7 | export type AllowMemesResult = { 8 | enable: boolean, 9 | errors: AFFError[], 10 | } 11 | 12 | const nonMemeSceneControlKind = [ 13 | "redline", "arcahvdistort", "arcahvdebris", 14 | "hidegroup", "enwidencamera", "enwidenlanes" 15 | ] 16 | 17 | const genAllowMemesResult = (file: AFFFile): AllowMemesResult => { 18 | const enableAllowMemesWithItem = (item: WithLocation): AllowMemesResult => ({ 19 | enable: true, 20 | errors: [{ 21 | message: "Allow memes mode is turned on since memes events present, some checks will be skipped", 22 | severity: DiagnosticSeverity.Hint, 23 | location: file.metadata.data.metaEndLocation, 24 | relatedInfo: [{ 25 | message: `The event that triggered the allow memes mode`, 26 | location: item.location 27 | }] 28 | }] 29 | }) 30 | for (const item of file.items) { 31 | if (item.data.kind === "camera") { 32 | return enableAllowMemesWithItem(item) 33 | } else if (item.data.kind === "scenecontrol") { 34 | if (!nonMemeSceneControlKind.includes(item.data.sceneControlKind.data.value)) { 35 | return enableAllowMemesWithItem(item) 36 | } 37 | } else if (item.data.kind === "arc") { 38 | if (item.data.colorId.data.value === 2) { 39 | return enableAllowMemesWithItem(item) 40 | } 41 | if (item.data.colorId.data.value === 3 && !isLine(item.data.lineKind.data)) { 42 | return enableAllowMemesWithItem(item) 43 | } 44 | if (item.data.lineKind.data.value === "designant") { 45 | return enableAllowMemesWithItem(item) 46 | } 47 | } else if (item.data.kind === "timinggroup") { 48 | if (timings.get(item.data).attributes.filter((attr) => /^angle[xy][0-9]+$/.test(attr)).length > 0) { 49 | return enableAllowMemesWithItem(item) 50 | } 51 | for (const nestedItem of item.data.items.data) { 52 | if (nestedItem.data.kind === "camera") { 53 | return enableAllowMemesWithItem(nestedItem) 54 | } else if (nestedItem.data.kind === "scenecontrol") { 55 | if (!nonMemeSceneControlKind.includes(nestedItem.data.sceneControlKind.data.value)) { 56 | return enableAllowMemesWithItem(nestedItem) 57 | } 58 | } else if (nestedItem.data.kind === "arc") { 59 | if (nestedItem.data.colorId.data.value === 2) { 60 | return enableAllowMemesWithItem(nestedItem) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | return { enable: false, errors: [] } 67 | } 68 | 69 | export const allowMemes = new AssociatedDataMap(genAllowMemesResult) -------------------------------------------------------------------------------- /server/src/associated-data/enwiden.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver"; 2 | import { AFFError, AFFFile, AFFSceneControlEvent, WithLocation } from "../types"; 3 | import { AssociatedDataMap } from "../util/associated-data"; 4 | 5 | export interface EnwidenData { 6 | time: number, 7 | enabled: boolean, 8 | item: WithLocation, 9 | } 10 | 11 | export type EnwidenResult = { 12 | cameras: EnwidenData[],//This should be sorted by time 13 | lanes: EnwidenData[],//This should be sorted by time 14 | errors: AFFError[], 15 | } 16 | 17 | const genEnwidenResult = (file: AFFFile): EnwidenResult => { 18 | let errors: AFFError[] = [] 19 | let rawCameras: EnwidenData[] = [] 20 | let rawLanes: EnwidenData[] = [] 21 | const processScenecontrol = (scenecontrol: WithLocation) => { 22 | const values = scenecontrol.data.values; 23 | if (values.data.length === 2) { 24 | if (values.data[0].data.kind == "float" && values.data[1].data.kind == "int") { 25 | const enabled = values.data[1].data.value > 0 26 | const data: EnwidenData = { 27 | time: scenecontrol.data.time.data.value + ( 28 | enabled ? 0 : values.data[0].data.value 29 | ), 30 | enabled, 31 | item: scenecontrol, 32 | } 33 | const kind = scenecontrol.data.sceneControlKind.data.value 34 | if (kind == "enwidencamera") { 35 | rawCameras.push(data) 36 | } else if (kind == "enwidenlanes") { 37 | rawLanes.push(data) 38 | } 39 | } 40 | } 41 | } 42 | for (const item of file.items) { 43 | if (item.data.kind === "scenecontrol") { 44 | processScenecontrol(item as WithLocation) 45 | } else if (item.data.kind === "timinggroup") { 46 | for (const nestedItem of item.data.items.data) { 47 | if (nestedItem.data.kind === "scenecontrol") { 48 | processScenecontrol(nestedItem as WithLocation) 49 | } 50 | } 51 | } 52 | } 53 | const filterEnwidenData = (rawData: EnwidenData[], type: string): EnwidenData[] => { 54 | const sortedData = [...rawData].sort((a, b) => a.time - b.time); 55 | const filteredData: EnwidenData[] = [] 56 | let enabled = false 57 | for (const data of sortedData) { 58 | if (enabled === data.enabled) { 59 | const enabledString = enabled ? "enabled" : "disabled" 60 | errors.push({ 61 | message: `The ${type} state is already ${enabledString}, you can't make it ${enabledString} again`, 62 | severity: DiagnosticSeverity.Warning, 63 | location: data.item.data.values.data[1].location, 64 | relatedInfo: [{ 65 | message: `Last time ${type} state becomes ${enabledString}`, 66 | location: filteredData.length > 0 ? filteredData[filteredData.length - 1].item.location : file.metadata.data.metaEndLocation 67 | }] 68 | }) 69 | } else { 70 | filteredData.push(data) 71 | enabled = data.enabled 72 | } 73 | } 74 | return sortedData 75 | } 76 | const cameras = filterEnwidenData(rawCameras, "enwidencamera") 77 | const lanes = filterEnwidenData(rawLanes, "enwidenlanes") 78 | return { 79 | errors, 80 | cameras, 81 | lanes, 82 | } 83 | } 84 | 85 | export const enwidens = new AssociatedDataMap(genEnwidenResult) -------------------------------------------------------------------------------- /server/src/associated-data/timing.ts: -------------------------------------------------------------------------------- 1 | import { AFFTimingEvent, AFFFile, AFFError, WithLocation, AFFTimingGroupEvent } from "../types" 2 | import { DiagnosticSeverity } from "vscode-languageserver"; 3 | import { AssociatedDataMap } from "../util/associated-data"; 4 | 5 | export interface TimingData { 6 | time: number, 7 | bpm: number, 8 | segment: number, 9 | item: WithLocation, 10 | } 11 | 12 | export type TimingResult = { 13 | datas: TimingData[],//This should be sorted by time 14 | attributes: string[], 15 | errors: AFFError[], 16 | } 17 | const genTimingResult = (group: AFFFile | AFFTimingGroupEvent): TimingResult => { 18 | let errors: AFFError[] = [] 19 | let datas = new Map() 20 | let items = ("kind" in group) ? group.items.data : group.items 21 | for (const item of items) { 22 | if (item.data.kind === "timing") { 23 | const time = item.data.time.data.value 24 | if (datas.has(time)) { 25 | errors.push({ 26 | message: `Another timing at this time is defined previously`, 27 | severity: DiagnosticSeverity.Error, 28 | location: item.location, 29 | relatedInfo: [{ 30 | message: `Previous timing definition`, 31 | location: datas.get(time).item.location 32 | }] 33 | }) 34 | } else { 35 | datas.set(time, { 36 | time, 37 | bpm: item.data.bpm.data.value, 38 | segment: item.data.measure.data.value, 39 | item: item as WithLocation, 40 | }) 41 | } 42 | } 43 | } 44 | const groupLocation = ("kind" in group) ? group.tagLocation : group.metadata.data.metaEndLocation 45 | if (datas.size <= 0) { 46 | errors.push({ 47 | message: `No timing event found ${("kind" in group) ? "in the timinggroup" : "outside timinggroups"}`, 48 | severity: DiagnosticSeverity.Error, 49 | location: groupLocation 50 | }) 51 | } else if (!datas.has(0)) { 52 | errors.push({ 53 | message: `No timing event at 0 time found ${("kind" in group) ? "in the timinggroup" : "outside timinggroups"}`, 54 | severity: DiagnosticSeverity.Warning, 55 | location: groupLocation 56 | }) 57 | } else { 58 | let firstZeroTiming = false 59 | if (items.length >= 0) { 60 | const first = items[0] 61 | if (first.data.kind === "timing") { 62 | if (first.data.time.data.value === 0) { 63 | firstZeroTiming = true 64 | } 65 | } 66 | } 67 | if (!firstZeroTiming) { 68 | errors.push({ 69 | message: `First item ${("kind" in group) ? "in the timinggroup" : "outside timinggroups"} is not timing event at 0 time`, 70 | severity: DiagnosticSeverity.Information, 71 | location: groupLocation 72 | }) 73 | } 74 | } 75 | const attributes=[] 76 | if("kind" in group){ 77 | if(group.timingGroupAttribute.data.value!==""){ 78 | attributes.push(...group.timingGroupAttribute.data.value.split("_")) 79 | } 80 | } 81 | return { datas: [...datas.values()].sort((a, b) => a.time - b.time), attributes, errors } 82 | } 83 | 84 | export const timings = new AssociatedDataMap(genTimingResult) -------------------------------------------------------------------------------- /server/src/checker/allow-memes.ts: -------------------------------------------------------------------------------- 1 | import { AFFChecker } from "../types" 2 | import { allowMemes } from "../associated-data/allow-memes" 3 | 4 | export const allowMemesChecker: AFFChecker = (file, error) => { 5 | error.splice(error.length, 0, ...allowMemes.get(file).errors) 6 | } -------------------------------------------------------------------------------- /server/src/checker/arc-position.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver" 2 | import { CstNodeLocation } from "chevrotain" 3 | import { AFFChecker, AFFError, AFFItem, isLine, WithLocation } from "../types" 4 | import { allowMemes } from "../associated-data/allow-memes" 5 | import { EnwidenData, enwidens } from "../associated-data/enwiden" 6 | import { upperBound } from "../util/misc" 7 | 8 | export const arcPositionChecker: AFFChecker = (file, errors) => { 9 | if (allowMemes.get(file).enable) { 10 | return 11 | } 12 | const cameras = enwidens.get(file).cameras 13 | for (const item of file.items) { 14 | checkItem(item, cameras, errors) 15 | } 16 | } 17 | 18 | const checkItem = ({ data, location }: WithLocation, cameras: EnwidenData[], errors: AFFError[]) => { 19 | if (data.kind === "arc") { 20 | const solid = !isLine(data.lineKind.data) 21 | checkPoint("start", solid, data.xStart.data.value, data.yStart.data.value, data.start.data.value, cameras, location, errors) 22 | checkPoint("end", solid, data.xEnd.data.value, data.yEnd.data.value, data.end.data.value, cameras, location, errors) 23 | } else if (data.kind === "timinggroup") { 24 | for (const item of data.items.data) { 25 | checkItem(item, cameras, errors) 26 | } 27 | } 28 | } 29 | 30 | const checkPoint = (tag: string, solid: boolean, x: number, y: number, time: number, cameras: EnwidenData[], location: CstNodeLocation, error: AFFError[]) => { 31 | let lastEnwidenCameraId = upperBound(cameras, time, (ec, t) => ec.time - t) - 1 32 | if (lastEnwidenCameraId >= 0) { 33 | if (!cameras[lastEnwidenCameraId].enabled && cameras[lastEnwidenCameraId].time === time) { 34 | lastEnwidenCameraId -= 1 35 | } 36 | } 37 | if (!(cameras[lastEnwidenCameraId]?.enabled ?? false)) { 38 | if ( 39 | Math.round(100 * y) > 100 || 40 | Math.round(100 * y) < 0 || 41 | Math.round(200 * x + 100 * y) > 300 || 42 | Math.round(200 * x - 100 * y) < -100 43 | ) { 44 | error.push({ 45 | message: `The ${tag} point of the ${solid ? "solid" : "tracking"} arc is out of the trapezium range`, 46 | severity: solid ? DiagnosticSeverity.Warning : DiagnosticSeverity.Information, 47 | location, 48 | }) 49 | } 50 | } else { 51 | if ( 52 | Math.round(100 * y) > 161 || 53 | Math.round(100 * y) < 0 || 54 | Math.round(16100 * x + 7500 * y) > 32200 || 55 | Math.round(16100 * x - 7500 * y) < -16100 56 | ) { 57 | error.push({ 58 | message: `The ${tag} point of the ${solid ? "solid" : "tracking"} arc is out of the trapezium range`, 59 | severity: solid ? DiagnosticSeverity.Warning : DiagnosticSeverity.Information, 60 | location, 61 | }) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /server/src/checker/cut-by-timing.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver" 2 | import { CstNodeLocation } from "chevrotain" 3 | import { timings, TimingData } from "../associated-data/timing" 4 | import { lowerBound, upperBound } from "../util/misc" 5 | import { AFFChecker, AFFError } from "../types" 6 | 7 | export const cutByTimingChecker: AFFChecker = (file, error) => { 8 | const timingData = timings.get(file).datas 9 | for (const item of file.items) { 10 | if (item.data.kind === "hold") { 11 | checkCutByTiming("hold", timingData, item.data.start.data.value, item.data.end.data.value, item.location, error) 12 | } else if (item.data.kind === "arc") { 13 | checkCutByTiming("arc", timingData, item.data.start.data.value, item.data.end.data.value, item.location, error) 14 | } else if (item.data.kind === "timinggroup") { 15 | const groupTimingData = timings.get(item.data).datas 16 | for (const nestedItem of item.data.items.data) { 17 | if (nestedItem.data.kind === "hold") { 18 | checkCutByTiming("hold", groupTimingData, nestedItem.data.start.data.value, nestedItem.data.end.data.value, nestedItem.location, error) 19 | } else if (nestedItem.data.kind === "arc") { 20 | checkCutByTiming("arc", groupTimingData, nestedItem.data.start.data.value, nestedItem.data.end.data.value, nestedItem.location, error) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | const checkCutByTiming = (tag: string, timingData: TimingData[], start: number, end: number, location: CstNodeLocation, error: AFFError[]) => { 28 | const firstTimingIndex = upperBound(timingData, start, (td, t) => td.time - t) 29 | const lastTimingIndex = lowerBound(timingData, end, (td, t) => td.time - t) 30 | const cuters = timingData.slice(firstTimingIndex, lastTimingIndex) 31 | if (cuters.length > 0) { 32 | error.push({ 33 | message: `The ${tag} item is cut by timing events`, 34 | severity: DiagnosticSeverity.Information, 35 | location, 36 | relatedInfo: cuters.map(timing => ({ 37 | message: `The timing event that cuts the ${tag} item`, 38 | location: timing.item.location, 39 | })), 40 | }) 41 | } 42 | } -------------------------------------------------------------------------------- /server/src/checker/enwiden.ts: -------------------------------------------------------------------------------- 1 | import { AFFChecker } from "../types" 2 | import { enwidens } from "../associated-data/enwiden" 3 | 4 | export const enwidenChecker: AFFChecker = (file, error) => { 5 | error.splice(error.length, 0, ...enwidens.get(file).errors) 6 | } -------------------------------------------------------------------------------- /server/src/checker/extra-lanes.ts: -------------------------------------------------------------------------------- 1 | 2 | import { DiagnosticSeverity } from "vscode-languageserver" 3 | import { EnwidenData, enwidens } from "../associated-data/enwiden" 4 | import { AFFChecker } from "../types" 5 | import { lowerBound, upperBound } from "../util/misc" 6 | 7 | export const extraLanesChecker: AFFChecker = (file, errors) => { 8 | const lanes = enwidens.get(file).lanes 9 | for (const item of file.items) { 10 | if (item.data.kind === "tap") { 11 | const trackId = item.data.trackId 12 | if (trackId.data.value === 0 || trackId.data.value === 5) { 13 | let lastEnwidenlaneId = upperBound(lanes, item.data.time.data.value, (ed, t) => ed.time - t) - 1 14 | if (lastEnwidenlaneId >= 0) { 15 | if (!lanes[lastEnwidenlaneId].enabled && lanes[lastEnwidenlaneId].time === item.data.time.data.value) { 16 | lastEnwidenlaneId -= 1 17 | } 18 | } 19 | if (!(lanes[lastEnwidenlaneId]?.enabled ?? false)) { 20 | errors.push({ 21 | message: `The tap item on the ${trackId.data.value} lane should not present when enwidenlanes is disabled`, 22 | severity: DiagnosticSeverity.Error, 23 | location: trackId.location, 24 | relatedInfo: [{ 25 | message: `The scenecontrol event that disable enwidenlanes`, 26 | location: lanes[lastEnwidenlaneId]?.item?.location ?? file.metadata.data.metaEndLocation, 27 | }], 28 | }) 29 | } 30 | } 31 | } else if (item.data.kind === "hold") { 32 | const trackId = item.data.trackId 33 | if (trackId.data.value === 0 || trackId.data.value === 5) { 34 | const firstEnwidenlaneId = upperBound(lanes, item.data.start.data.value, (ed, t) => ed.time - t) - 1 35 | const lastEnwidenlaneId = lowerBound(lanes, item.data.end.data.value, (ed, t) => ed.time - t) 36 | const disabler: (EnwidenData | null)[] = lanes.slice(Math.max(firstEnwidenlaneId, 0), lastEnwidenlaneId) 37 | .filter((lane) => !lane.enabled) 38 | if (firstEnwidenlaneId < 0) { 39 | disabler.unshift(null) 40 | } 41 | if (disabler.length > 0) { 42 | errors.push({ 43 | message: `The hold item on the ${trackId.data.value} lane should not present when enwidenlanes is disabled`, 44 | severity: DiagnosticSeverity.Error, 45 | location: trackId.location, 46 | relatedInfo: disabler.map(lane => ({ 47 | message: `The scenecontrol event that disable enwidenlanes`, 48 | location: lane?.item?.location ?? file.metadata.data.metaEndLocation, 49 | })), 50 | }) 51 | 52 | } 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /server/src/checker/float-digit.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver" 2 | import { AFFChecker, AFFFloat, AFFError, WithLocation, AFFItem } from "../types"; 3 | 4 | export const floatDigitChecker: AFFChecker = (file, errors) => { 5 | for (const item of file.items) { 6 | checkItem(item, errors) 7 | } 8 | } 9 | 10 | const checkItem = ({ data, location }: WithLocation, errors: AFFError[]) => { 11 | if (data.kind === "timing") { 12 | checkFloat(data.bpm, errors) 13 | checkFloat(data.measure, errors) 14 | } else if (data.kind === "arc") { 15 | checkFloat(data.xStart, errors) 16 | checkFloat(data.xEnd, errors) 17 | checkFloat(data.yStart, errors) 18 | checkFloat(data.yEnd, errors) 19 | } else if (data.kind === "camera") { 20 | checkFloat(data.translationX, errors) 21 | checkFloat(data.translationY, errors) 22 | checkFloat(data.translationZ, errors) 23 | checkFloat(data.rotationX, errors) 24 | checkFloat(data.rotationY, errors) 25 | checkFloat(data.rotationZ, errors) 26 | } else if (data.kind === "scenecontrol") { 27 | for (const value of data.values.data) { 28 | if (value.data.kind === "float") { 29 | checkFloat(value as WithLocation, errors) 30 | } 31 | } 32 | } else if (data.kind === "timinggroup") { 33 | for (const item of data.items.data) { 34 | checkItem(item, errors) 35 | } 36 | } 37 | } 38 | 39 | const checkFloat = (float: WithLocation, errors: AFFError[]) => { 40 | if (float.data.digit !== 2) { 41 | errors.push({ 42 | message: `Float values should have exact 2 digits in its fractional part`, 43 | severity: DiagnosticSeverity.Warning, 44 | location: float.location, 45 | }) 46 | } 47 | } -------------------------------------------------------------------------------- /server/src/checker/metadata.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver" 2 | import { AFFChecker } from "../types" 3 | 4 | export const metadataChecker: AFFChecker = (file, errors) => { 5 | for (const entry of file.metadata.data.data.values()) { 6 | if (!["AudioOffset", "TimingPointDensityFactor"].includes(entry.data.key.data)) { 7 | errors.push({ 8 | message: `The "${entry.data.key.data}" metadata is not used and will be ignored`, 9 | severity: DiagnosticSeverity.Warning, 10 | location: entry.data.key.location, 11 | }) 12 | } 13 | } 14 | if (!file.metadata.data.data.has("AudioOffset")) { 15 | errors.push({ 16 | message: `The "AudioOffset" metadata is missing, this chart will be processed with zero audio offset`, 17 | severity: DiagnosticSeverity.Warning, 18 | location: file.metadata.data.metaEndLocation, 19 | }) 20 | } else { 21 | const offset = file.metadata.data.data.get("AudioOffset") 22 | if (!offset.data.value.data.match(/^-?(?:0|[1-9][0-9]*)$/)) { 23 | errors.push({ 24 | message: `The value of "AudioOffset" metadata is not an int`, 25 | severity: DiagnosticSeverity.Error, 26 | location: offset.data.value.location, 27 | }) 28 | } 29 | } 30 | if (file.metadata.data.data.has("TimingPointDensityFactor")) { 31 | const factor = file.metadata.data.data.get("TimingPointDensityFactor") 32 | const factorValue = parseFloat(factor.data.value.data) 33 | if (isNaN(factorValue)) { 34 | errors.push({ 35 | message: `The value of "TimingPointDensityFactor" metadata is not an float`, 36 | severity: DiagnosticSeverity.Error, 37 | location: factor.data.value.location, 38 | }) 39 | } else if (factorValue <= 0) { 40 | errors.push({ 41 | message: `The value of "TimingPointDensityFactor" metadata is not positive`, 42 | severity: DiagnosticSeverity.Error, 43 | location: factor.data.value.location, 44 | }) 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /server/src/checker/overlap.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity, Location } from "vscode-languageserver" 2 | import { CstNodeLocation } from "chevrotain" 3 | import { AFFChecker, AFFError, AFFTrackItem, WithLocation, AFFTrackIdValue, AFFCameraEvent, AFFItem, AFFSceneControlEvent, AFFFloat } from "../types" 4 | import { timings } from "../associated-data/timing" 5 | 6 | export const overlapChecker: AFFChecker = (file, error) => { 7 | const trackRecord = new Map[]>() 8 | const checkItem = (item: WithLocation) => { 9 | if (item.data.kind === "arc") { 10 | const arctaps = item.data.arctaps 11 | if (arctaps) { 12 | // Note: this only check arctaps on one arc and on the same time 13 | // If it is needed to check for "near" on time arctap we should rewrite this 14 | // There is no plan to add overlap check for arctaps across different arcs 15 | let timestamps = new Map() 16 | for (const arctap of arctaps.data) { 17 | const timestamp = arctap.data.time.data.value 18 | if (timestamps.has(timestamp)) { 19 | error.push({ 20 | message: `The arctap is duplicated with previous arctap`, 21 | severity: DiagnosticSeverity.Error, 22 | location: arctap.location, 23 | relatedInfo: [{ 24 | message: `Previous arctap`, 25 | location: timestamps.get(timestamp) 26 | }] 27 | }) 28 | } else { 29 | timestamps.set(timestamp, arctap.location) 30 | } 31 | } 32 | } 33 | } else if (item.data.kind === "tap" || item.data.kind === "hold") { 34 | const trackId = item.data.trackId.data.value 35 | if (!trackRecord.has(trackId)) { 36 | trackRecord.set(trackId, []) 37 | } 38 | trackRecord.get(trackId).push(item as WithLocation) 39 | } else if (item.data.kind === "timinggroup") { 40 | if (!timings.get(item.data).attributes.includes("noinput")) { 41 | for (const nestedItem of item.data.items.data) { 42 | checkItem(nestedItem) 43 | } 44 | } 45 | } 46 | } 47 | for (const item of file.items) { 48 | checkItem(item) 49 | } 50 | for (const items of trackRecord.values()) { 51 | checkTrackOverlap(error, items) 52 | } 53 | const cameras: WithLocation[] = [] 54 | for (const item of file.items) { 55 | if (item.data.kind === "camera") { 56 | cameras.push(item as WithLocation) 57 | } else if (item.data.kind === "timinggroup") { 58 | for (const nestedItem of item.data.items.data) { 59 | if (nestedItem.data.kind === "camera") { 60 | cameras.push(nestedItem as WithLocation) 61 | } 62 | } 63 | } 64 | } 65 | checkCameraOverlap(error, cameras) 66 | const scenecontrolKinds = new Map[]>() 67 | const processScenecontrol = (scenecontrol: WithLocation) => { 68 | const kind = scenecontrol.data.sceneControlKind.data.value 69 | if (["enwidencamera", "enwidenlanes", "trackdisplay"].includes(kind)) { 70 | const values = scenecontrol.data.values; 71 | if (values.data.length === 2) { 72 | if (values.data[0].data.kind == "float" && values.data[1].data.kind == "int") { 73 | if (!scenecontrolKinds.has(kind)) { 74 | scenecontrolKinds.set(kind, []) 75 | } 76 | scenecontrolKinds.get(kind).push(scenecontrol) 77 | } 78 | } 79 | } 80 | } 81 | for (const item of file.items) { 82 | if (item.data.kind === "scenecontrol") { 83 | processScenecontrol(item as WithLocation) 84 | } else if (item.data.kind === "timinggroup") { 85 | for (const nestedItem of item.data.items.data) { 86 | if (nestedItem.data.kind === "scenecontrol") { 87 | processScenecontrol(nestedItem as WithLocation) 88 | } 89 | } 90 | } 91 | } 92 | const timeScaleMap = new Map() 93 | timeScaleMap.set("enwidencamera", 1) 94 | timeScaleMap.set("enwidenlanes", 1) 95 | timeScaleMap.set("trackdisplay", 1000) 96 | for (const [kind, scenecontrols] of scenecontrolKinds) { 97 | checkScenecontrolOverlap(error, scenecontrols, kind, timeScaleMap.get(kind)) 98 | } 99 | } 100 | 101 | const checkTrackOverlap = (error: AFFError[], items: WithLocation[]) => { 102 | const getStart = (item: WithLocation) => item.data.kind === "tap" ? item.data.time.data.value : item.data.start.data.value 103 | const report = (location: CstNodeLocation, lastLocation: CstNodeLocation) => { 104 | error.push({ 105 | message: `The track item is overlapped with a previous track item`, 106 | severity: DiagnosticSeverity.Error, 107 | location, 108 | relatedInfo: [{ 109 | message: `The previous track item`, 110 | location: lastLocation 111 | }] 112 | }) 113 | } 114 | const sortedByStart = items.sort((a, b) => getStart(a) - getStart(b)) 115 | // Note: may be there are more thing to save if we want an auto-fix feature 116 | let lastLocation: CstNodeLocation | null = null 117 | let lastEnd: number = -Infinity 118 | let closed: boolean = false 119 | for (const item of sortedByStart) { 120 | if (item.data.kind === "tap") { 121 | const time = item.data.time.data.value 122 | if (time < lastEnd || (time === lastEnd && closed)) { 123 | report(item.location, lastLocation) 124 | } 125 | if (time >= lastEnd) { 126 | lastLocation = item.location 127 | lastEnd = time 128 | closed = true 129 | } 130 | } else { 131 | const start = item.data.start.data.value 132 | if (start < lastEnd || (start === lastEnd && closed)) { 133 | report(item.location, lastLocation) 134 | } 135 | const end = item.data.end.data.value 136 | if (end > lastEnd) { 137 | lastLocation = item.location 138 | lastEnd = end 139 | closed = false 140 | } 141 | } 142 | } 143 | } 144 | 145 | const checkCameraOverlap = (error: AFFError[], cameras: WithLocation[]) => { 146 | const report = (location: CstNodeLocation, lastLocation: CstNodeLocation) => { 147 | error.push({ 148 | message: `The camera item is overlapped with a previous camera item`, 149 | severity: DiagnosticSeverity.Warning, 150 | location, 151 | relatedInfo: [{ 152 | message: `The previous camera item`, 153 | location: lastLocation 154 | }] 155 | }) 156 | } 157 | const sortedByStart = cameras.sort((a, b) => a.data.time.data.value - b.data.time.data.value) 158 | let lastLocation: CstNodeLocation | null = null 159 | let lastEnd: number = -Infinity 160 | for (const item of sortedByStart) { 161 | const start = item.data.time.data.value 162 | if (start < lastEnd) { 163 | report(item.location, lastLocation) 164 | } 165 | const end = start + item.data.duration.data.value 166 | if (end > lastEnd) { 167 | lastLocation = item.location 168 | lastEnd = end 169 | } 170 | } 171 | } 172 | 173 | const checkScenecontrolOverlap = (error: AFFError[], scenecontrols: WithLocation[], kind: string, timeScale: number) => { 174 | const report = (location: CstNodeLocation, lastLocation: CstNodeLocation) => { 175 | error.push({ 176 | message: `The scenecontrol item with kind "${kind}" is overlapped with a previous scenecontrol item with kind "${kind}"`, 177 | severity: DiagnosticSeverity.Warning, 178 | location, 179 | relatedInfo: [{ 180 | message: `The previous scenecontrol item`, 181 | location: lastLocation 182 | }] 183 | }) 184 | } 185 | const sortedByStart = scenecontrols.sort((a, b) => a.data.time.data.value - b.data.time.data.value) 186 | let lastLocation: CstNodeLocation | null = null 187 | let lastEnd: number = -Infinity 188 | for (const item of sortedByStart) { 189 | const start = item.data.time.data.value 190 | if (start < lastEnd) { 191 | report(item.location, lastLocation) 192 | } 193 | const end = start + (item.data.values.data[0] as WithLocation).data.value * timeScale 194 | if (end > lastEnd) { 195 | lastLocation = item.location 196 | lastEnd = end 197 | } 198 | } 199 | } -------------------------------------------------------------------------------- /server/src/checker/scenecontrol.ts: -------------------------------------------------------------------------------- 1 | import { AFFChecker, WithLocation, AFFSceneControlKind, AFFValue, AFFError, AFFValues } from "../types" 2 | import { DiagnosticSeverity } from "vscode-languageserver" 3 | import { CstNodeLocation } from "chevrotain" 4 | 5 | export const scenecontrolChecker: AFFChecker = (file, errors) => { 6 | for (const { data } of file.items) { 7 | if (data.kind === "scenecontrol") { 8 | checkScenecontrol(data.sceneControlKind, data.values, errors) 9 | } else if (data.kind === "timinggroup") { 10 | for (const nestedItem of data.items.data) { 11 | if (nestedItem.data.kind === "scenecontrol") { 12 | checkScenecontrol(nestedItem.data.sceneControlKind, nestedItem.data.values, errors) 13 | } 14 | } 15 | } 16 | } 17 | } 18 | 19 | const checkScenecontrol = (kind: WithLocation, values: WithLocation[]>, error: AFFError[]) => { 20 | if (kind.data.value === "trackshow" || kind.data.value === "trackhide") { 21 | checkValuesCount(error, kind.data.value, 0, values.data, values.location) 22 | return 23 | } 24 | if ( 25 | kind.data.value === "redline" || 26 | kind.data.value === "arcahvdistort" || 27 | kind.data.value === "arcahvdebris" || 28 | kind.data.value === "hidegroup" || 29 | kind.data.value === "enwidencamera" || 30 | kind.data.value === "enwidenlanes" || 31 | kind.data.value === "trackdisplay" 32 | ) { 33 | if (checkValuesCount(error, kind.data.value, 2, values.data, values.location)) { 34 | checkValueType(error, kind.data.value, "length", "float", values.data, 0) 35 | checkValueType(error, kind.data.value, "value", "int", values.data, 1) 36 | } 37 | return 38 | } 39 | error.push({ 40 | message: `Scenecontrol event with type "${kind.data.value}" is not known by us, so the type of additional values is not checked`, 41 | location: kind.location, 42 | severity: DiagnosticSeverity.Warning, 43 | }) 44 | } 45 | 46 | const checkValuesCount = (errors: AFFError[], kind: string, count: number, values: WithLocation[], valuesLocation: CstNodeLocation): boolean => { 47 | if (values.length !== count) { 48 | // error: value count mismatch 49 | errors.push({ 50 | message: `Scenecontrol event with type "${kind}" should have ${count} additional value(s) instead of ${values.length} additional value(s)`, 51 | location: valuesLocation, 52 | severity: DiagnosticSeverity.Error, 53 | }) 54 | return false 55 | } 56 | return true 57 | } 58 | 59 | const checkValueType = ( 60 | errors: AFFError[], 61 | eventKind: string, 62 | fieldName: string, 63 | kind: T, 64 | values: WithLocation[], 65 | id: number 66 | ): WithLocation | null => { 67 | const value = values[id] 68 | if (value.data.kind !== kind) { 69 | // error: value type mismatch 70 | errors.push({ 71 | message: `The value in the "${fieldName}" field of scenecontrol event with type "${eventKind}" should be "${kind}" instead of "${value.data.kind}"`, 72 | location: values[id].location, 73 | severity: DiagnosticSeverity.Error, 74 | }) 75 | return null 76 | } else { 77 | return value as WithLocation 78 | } 79 | } -------------------------------------------------------------------------------- /server/src/checker/timing.ts: -------------------------------------------------------------------------------- 1 | import { AFFChecker } from "../types" 2 | import { timings } from "../associated-data/timing" 3 | 4 | export const timingChecker: AFFChecker = (file, error) => { 5 | error.splice(error.length, 0, ...timings.get(file).errors) 6 | for (const item of file.items) { 7 | if (item.data.kind === "timinggroup") { 8 | error.splice(error.length, 0, ...timings.get(item.data).errors) 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /server/src/checker/timinggroup-attribute.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver"; 2 | import { timings } from "../associated-data/timing"; 3 | import { AFFChecker } from "../types"; 4 | 5 | export const timinggroupAttributeChecker: AFFChecker = (file, errors) => { 6 | for (const { data } of file.items) { 7 | if (data.kind === "timinggroup") { 8 | const attribute=timings.get(data).attributes 9 | const unknownAttribute=attribute 10 | .filter((attr)=>attr!=="noinput" && attr!=="fadingholds" ) 11 | .filter((attr)=>!/^angle[xy][0-9]+$/.test(attr)) 12 | if(unknownAttribute.length>0){ 13 | errors.push({ 14 | message: `Timinggroup event with attribute ${unknownAttribute.map(attr=>`"${attr}"`).join(", ")} is not known by us`, 15 | location: data.timingGroupAttribute.location, 16 | severity: DiagnosticSeverity.Warning, 17 | }) 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /server/src/checker/value-range.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver" 2 | import { AFFChecker, AFFInt, AFFError, WithLocation, AFFItem, isLine } from "../types" 3 | 4 | export const valueRangeChecker: AFFChecker = (file, errors) => { 5 | for (const item of file.items) { 6 | checkItem(item, errors) 7 | } 8 | } 9 | 10 | const checkItem = ({ data, location }: WithLocation, errors: AFFError[]) => { 11 | if (data.kind === "timing") { 12 | checkTimestamp(data.time, errors) 13 | if (data.bpm.data.value !== 0 && data.measure.data.value === 0) { 14 | errors.push({ 15 | message: `Timing event with non-zero bpm should not have zero beats per segment`, 16 | severity: DiagnosticSeverity.Error, 17 | location: data.measure.location, 18 | }) 19 | } 20 | if (data.bpm.data.value === 0 && data.measure.data.value !== 0) { 21 | errors.push({ 22 | message: `Timing event with zero bpm should have zero beats per segment`, 23 | severity: DiagnosticSeverity.Information, 24 | location: data.measure.location, 25 | }) 26 | } 27 | } else if (data.kind === "tap") { 28 | checkTimestamp(data.time, errors) 29 | } else if (data.kind === "hold") { 30 | checkTimestamp(data.start, errors) 31 | checkTimestamp(data.end, errors) 32 | if (data.start.data.value >= data.end.data.value) { 33 | errors.push({ 34 | message: `Hold event should have a positive time length`, 35 | severity: DiagnosticSeverity.Error, 36 | location: location, 37 | }) 38 | } 39 | } else if (data.kind === "arc") { 40 | checkTimestamp(data.start, errors) 41 | checkTimestamp(data.end, errors) 42 | if (data.start.data.value > data.end.data.value) { 43 | errors.push({ 44 | message: `Arc event should have a non-negative time length`, 45 | severity: DiagnosticSeverity.Error, 46 | location: location, 47 | }) 48 | } 49 | if (data.start.data.value === data.end.data.value) { 50 | if (data.xStart.data.value === data.xEnd.data.value && data.yStart.data.value === data.yEnd.data.value) { 51 | errors.push({ 52 | message: `Arc event with zero time length should have different start point and end point`, 53 | severity: DiagnosticSeverity.Error, 54 | location: location, 55 | }) 56 | } 57 | if (data.curveKind.data.value !== "s") { 58 | errors.push({ 59 | message: `Arc event with zero time length should be "s" type`, 60 | severity: DiagnosticSeverity.Information, 61 | location: data.curveKind.location, 62 | }) 63 | } 64 | if (data.arctaps) { 65 | errors.push({ 66 | message: `Arc event with zero time length should not have arctap events on it`, 67 | severity: DiagnosticSeverity.Error, 68 | location: data.arctaps.location, 69 | }) 70 | } 71 | } 72 | if (data.effect.data.value !== "none" && !data.effect.data.value.endsWith("_wav")) { 73 | errors.push({ 74 | message: `Arc event with effect "${data.effect.data.value}" is not known by us`, 75 | severity: DiagnosticSeverity.Warning, 76 | location: data.effect.location, 77 | }) 78 | } 79 | if (!isLine(data.lineKind.data) && data.arctaps) { 80 | errors.push({ 81 | message: `Arc event with arctap events on it will be treated as not solid even it is specified as solid`, 82 | severity: DiagnosticSeverity.Warning, 83 | location: data.lineKind.location, 84 | }) 85 | } 86 | if (!isLine(data.lineKind.data) && data.arctaps === undefined && data.colorId.data.value >= 4) { 87 | errors.push({ 88 | message: `Solid arc event should not use the color ${data.colorId.data.value}`, 89 | severity: DiagnosticSeverity.Error, 90 | location: data.colorId.location, 91 | }) 92 | } 93 | if (data.arctaps) { 94 | for (const arctap of data.arctaps.data) { 95 | if (arctap.data.time.data.value < data.start.data.value || arctap.data.time.data.value > data.end.data.value) { 96 | errors.push({ 97 | message: `Arctap event should happens in the time range of parent arc event`, 98 | severity: DiagnosticSeverity.Error, 99 | location: arctap.location, 100 | }) 101 | } 102 | } 103 | } 104 | } else if (data.kind === "camera") { 105 | if (data.duration.data.value < 0) { 106 | errors.push({ 107 | message: `Camera event should have non negative duration`, 108 | severity: DiagnosticSeverity.Error, 109 | location: data.duration.location, 110 | }) 111 | } 112 | } else if (data.kind === "scenecontrol") { 113 | const kind = data.sceneControlKind.data.value 114 | if (["enwidencamera", "enwidenlanes", "trackdisplay"].includes(kind)) { 115 | const values = data.values; 116 | if (values.data.length === 2) { 117 | if (values.data[0].data.kind == "float" && values.data[1].data.kind == "int") { 118 | if (values.data[0].data.value <= 0) { 119 | errors.push({ 120 | message: `The scenecontrol item with kind "${kind}" should have non negative duration`, 121 | severity: DiagnosticSeverity.Error, 122 | location: values.data[0].location, 123 | }) 124 | } 125 | } 126 | } 127 | } 128 | } else if (data.kind === "timinggroup") { 129 | for (const item of data.items.data) { 130 | checkItem(item, errors) 131 | } 132 | } 133 | } 134 | 135 | const checkTimestamp = (timestamp: WithLocation, errors: AFFError[]) => { 136 | if (timestamp.data.value < 0) { 137 | errors.push({ 138 | message: `Timestamp should not be negative`, 139 | severity: DiagnosticSeverity.Error, 140 | location: timestamp.location, 141 | }) 142 | } 143 | } -------------------------------------------------------------------------------- /server/src/checkers.ts: -------------------------------------------------------------------------------- 1 | import { allowMemesChecker } from "./checker/allow-memes" 2 | import { metadataChecker } from "./checker/metadata" 3 | import { valueRangeChecker } from "./checker/value-range" 4 | import { floatDigitChecker } from "./checker/float-digit" 5 | import { timingChecker } from "./checker/timing" 6 | import { arcPositionChecker } from "./checker/arc-position" 7 | import { overlapChecker } from "./checker/overlap" 8 | import { cutByTimingChecker } from "./checker/cut-by-timing" 9 | import { scenecontrolChecker } from "./checker/scenecontrol" 10 | import { AFFFile, AFFError } from "./types" 11 | import { timinggroupAttributeChecker } from "./checker/timinggroup-attribute" 12 | import { enwidenChecker } from "./checker/enwiden" 13 | import { extraLanesChecker } from "./checker/extra-lanes" 14 | 15 | const checkers = [ 16 | allowMemesChecker, 17 | metadataChecker, 18 | valueRangeChecker, 19 | floatDigitChecker, 20 | timingChecker, 21 | enwidenChecker, 22 | arcPositionChecker, 23 | overlapChecker, 24 | cutByTimingChecker, 25 | extraLanesChecker, 26 | scenecontrolChecker, 27 | timinggroupAttributeChecker, 28 | ] 29 | 30 | export const processCheckers = (file: AFFFile): AFFError[] => { 31 | let errors: AFFError[] = [] 32 | for (const checker of checkers) { 33 | checker(file, errors) 34 | } 35 | return errors 36 | } -------------------------------------------------------------------------------- /server/src/lang.ts: -------------------------------------------------------------------------------- 1 | import { AFFLexer } from "./lexer" 2 | import * as lsp from "vscode-languageserver" 3 | import { affParser } from "./parser" 4 | import { EOF } from "chevrotain" 5 | import { affToAST } from "./to-ast" 6 | import { AFFError } from "./types" 7 | import { processCheckers } from "./checkers" 8 | import { TextDocument } from "vscode-languageserver-textdocument" 9 | 10 | export const checkAFF = (content: TextDocument): lsp.Diagnostic[] => { 11 | let errors: lsp.Diagnostic[] = [] 12 | const text = content.getText() 13 | const lexingResult = AFFLexer.tokenize(text) 14 | if (lexingResult.errors.length > 0) { 15 | errors = errors.concat(lexingResult.errors.map(e => ({ 16 | severity: lsp.DiagnosticSeverity.Error, 17 | message: e.message, 18 | range: { 19 | start: { line: e.line - 1, character: e.column - 1 }, 20 | end: content.positionAt(content.offsetAt({ line: e.line - 1, character: e.column - 1 }) + e.length) 21 | } 22 | }))) 23 | } 24 | // The error tokens is just ignored so we can find more errors in parsing stage 25 | affParser.input = lexingResult.tokens 26 | const parsingResult = affParser.aff() 27 | if (affParser.errors.length > 0) { 28 | errors = errors.concat(affParser.errors.map(e => ({ 29 | severity: lsp.DiagnosticSeverity.Error, 30 | message: e.message, 31 | range: e.token.tokenType === EOF ? { 32 | start: content.positionAt(text.length), 33 | end: content.positionAt(text.length), 34 | } : { 35 | start: { line: e.token.startLine - 1, character: e.token.startColumn - 1 }, 36 | end: { line: e.token.endLine - 1, character: e.token.endColumn }, 37 | } 38 | }) 39 | )) 40 | } else { 41 | const astResult = affToAST(parsingResult) 42 | errors = errors.concat(astResult.errors.map(e => transformAFFError(e, content.uri))) 43 | const checkerErrors = processCheckers(astResult.ast) 44 | errors = errors.concat(checkerErrors.map(e => transformAFFError(e, content.uri))) 45 | } 46 | return errors 47 | } 48 | 49 | const transformAFFError = (e: AFFError, uri: string): lsp.Diagnostic => ({ 50 | severity: e.severity, 51 | message: e.message, 52 | range: { 53 | start: { line: e.location.startLine - 1, character: e.location.startColumn - 1 }, 54 | end: { line: e.location.endLine - 1, character: e.location.endColumn }, 55 | }, 56 | relatedInformation: e.relatedInfo ? e.relatedInfo.map(info => ({ 57 | message: info.message, 58 | location: { 59 | uri: uri, 60 | range: { 61 | start: { line: info.location.startLine - 1, character: info.location.startColumn - 1 }, 62 | end: { line: info.location.endLine - 1, character: info.location.endColumn }, 63 | } 64 | }, 65 | })) : undefined 66 | }) -------------------------------------------------------------------------------- /server/src/lexer.ts: -------------------------------------------------------------------------------- 1 | import { createToken, Lexer } from "chevrotain"; 2 | 3 | const endline = createToken({ name: "endline", pattern: /\r\n|\r|\n/, line_breaks: true }) 4 | 5 | const colon = createToken({ name: "colon", pattern: /:/, label: ":", push_mode: "data" }) 6 | const key = createToken({ name: "key", pattern: /[^:\r\n]+/ }) 7 | const data = createToken({ name: "data", pattern: /[^\r\n]+/, pop_mode: true }) 8 | const metaEnd = createToken({ name: "metaEnd", pattern: /-(?:\r\n|\r|\n)/, push_mode: "main", line_breaks: true, label: "-" }) 9 | 10 | const lParen = createToken({ name: "lParen", pattern: /\(/, label: "(" }) 11 | const rParen = createToken({ name: "rParen", pattern: /\)/, label: ")" }) 12 | const lBrack = createToken({ name: "lBrack", pattern: /\[/, label: "[" }) 13 | const rBrack = createToken({ name: "rBrack", pattern: /\]/, label: "]" }) 14 | const lBrace = createToken({ name: "lBrace", pattern: /\{/, label: "{" }) 15 | const rBrace = createToken({ name: "rBrace", pattern: /\}/, label: "}" }) 16 | const comma = createToken({ name: "comma", pattern: /,/, label: "," }) 17 | const semicolon = createToken({ name: "semicolon", pattern: /;/, label: ";" }) 18 | 19 | const value = createToken({ name: "value", pattern: Lexer.NA }) 20 | const word = createToken({ name: "word", pattern: /[a-zA-Z][a-zA-Z0-9_]*/, categories: value }) 21 | const float = createToken({ name: "float", pattern: /-?[0-9]+\.[0-9]+/, categories: value }) 22 | const int = createToken({ name: "int", pattern: /-?(?:0|[1-9][0-9]*)/, categories: value }) 23 | 24 | // \s without \r\n 25 | const whitespace = createToken({ 26 | name: "whitespace", 27 | pattern: /\s+/, 28 | group: "whitespace", 29 | }) 30 | 31 | export const tokenTypes = { endline, colon, key, data, metaEnd, lParen, rParen, lBrack, rBrack, lBrace, rBrace, comma, semicolon, value, word, float, int } 32 | export const AFFLexer = new Lexer({ 33 | modes: { 34 | meta: [endline, metaEnd, colon, key], 35 | data: [data], 36 | main: [whitespace, lParen, rParen, lBrack, rBrack, lBrace, rBrace, comma, semicolon, float, int, word] 37 | }, 38 | defaultMode: "meta" 39 | }) -------------------------------------------------------------------------------- /server/src/parser.ts: -------------------------------------------------------------------------------- 1 | import { CstParser } from "chevrotain"; 2 | import { tokenTypes } from "./lexer"; 3 | 4 | class AFFParser extends CstParser { 5 | public metadataEntry = this.RULE("metadataEntry", () => { 6 | this.CONSUME(tokenTypes.key) 7 | this.CONSUME(tokenTypes.colon) 8 | this.CONSUME(tokenTypes.data) 9 | this.CONSUME(tokenTypes.endline) 10 | }) 11 | public metadata = this.RULE("metadata", () => { 12 | this.MANY(() => this.SUBRULE(this.metadataEntry)) 13 | this.CONSUME(tokenTypes.metaEnd) 14 | }) 15 | public values = this.RULE("values", () => { 16 | this.CONSUME(tokenTypes.lParen) 17 | this.MANY_SEP({ 18 | SEP: tokenTypes.comma, 19 | DEF: () => { 20 | this.CONSUME(tokenTypes.value) 21 | } 22 | }) 23 | this.CONSUME(tokenTypes.rParen) 24 | }) 25 | public event = this.RULE("event", () => { 26 | this.OPTION(() => this.CONSUME(tokenTypes.word)) 27 | this.SUBRULE(this.values) 28 | this.OPTION1(() => this.SUBRULE(this.subevents)) 29 | this.OPTION2(() => this.SUBRULE(this.segment)) 30 | }) 31 | public subevents = this.RULE("subevents", () => { 32 | this.CONSUME(tokenTypes.lBrack) 33 | this.MANY_SEP({ 34 | SEP: tokenTypes.comma, 35 | DEF: () => { 36 | this.SUBRULE(this.event) 37 | } 38 | }) 39 | this.CONSUME(tokenTypes.rBrack) 40 | }) 41 | public segment = this.RULE("segment", () => { 42 | this.CONSUME(tokenTypes.lBrace) 43 | this.SUBRULE(this.items) 44 | this.CONSUME(tokenTypes.rBrace) 45 | }) 46 | public item = this.RULE("item", () => { 47 | this.SUBRULE(this.event) 48 | this.CONSUME(tokenTypes.semicolon) 49 | }) 50 | public items = this.RULE("items", () => { 51 | this.MANY({ 52 | DEF: () => this.SUBRULE(this.item) 53 | }) 54 | }) 55 | public aff = this.RULE("aff", () => { 56 | this.SUBRULE(this.metadata) 57 | this.SUBRULE(this.items) 58 | }) 59 | constructor() { 60 | // see https://sap.github.io/chevrotain/docs/tutorial/step4_fault_tolerance.html for the error recovery heuristics 61 | super(tokenTypes, { recoveryEnabled: true, nodeLocationTracking: "full" }) 62 | this.performSelfAnalysis() 63 | } 64 | } 65 | 66 | export const affParser = new AFFParser() 67 | export const BaseAffVisitor = affParser.getBaseCstVisitorConstructorWithDefaults() -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import * as lsp from "vscode-languageserver/node" 2 | import { TextDocument } from 'vscode-languageserver-textdocument' 3 | import { checkAFF } from "./lang" 4 | import { DiagnosticSeverity, TextDocumentSyncKind } from "vscode-languageserver" 5 | 6 | let connection = lsp.createConnection() 7 | 8 | let documents = new lsp.TextDocuments(TextDocument) 9 | 10 | connection.onInitialize((params) => { 11 | return { capabilities: { textDocumentSync: TextDocumentSyncKind.Full } } 12 | }) 13 | 14 | connection.onInitialized(() => { 15 | connection.client.register(lsp.DidChangeConfigurationNotification.type, undefined); 16 | }) 17 | 18 | connection.onDidChangeConfiguration(change => { 19 | console.log(`change config`) 20 | documents.all().forEach(validateTextDocument); 21 | }) 22 | 23 | documents.onDidChangeContent((change) => { 24 | console.log(`change ${change.document.uri}`) 25 | validateTextDocument(change.document) 26 | }) 27 | 28 | documents.onDidClose((change) => { 29 | console.log(`close ${change.document.uri}`) 30 | connection.sendDiagnostics({ 31 | uri: change.document.uri, diagnostics: [] 32 | }) 33 | }) 34 | 35 | const getSetting = async (uri: string) => await connection.workspace.getConfiguration({ scopeUri: uri, section: "arcaeaFileFormat" }) 36 | 37 | const validateTextDocument = async (textDocument: TextDocument) => { 38 | const level = (await getSetting(textDocument.uri)).diagnosticLevel 39 | const errors = checkAFF(textDocument).filter((e) => { 40 | if (level == "warn") { 41 | return e.severity != DiagnosticSeverity.Information 42 | } else if (level == "error") { 43 | return e.severity != DiagnosticSeverity.Information && e.severity != DiagnosticSeverity.Warning 44 | } 45 | return true 46 | }) 47 | connection.sendDiagnostics({ 48 | uri: textDocument.uri, diagnostics: errors 49 | }) 50 | } 51 | 52 | documents.listen(connection) 53 | 54 | connection.listen() -------------------------------------------------------------------------------- /server/src/to-ast.ts: -------------------------------------------------------------------------------- 1 | import { DiagnosticSeverity } from "vscode-languageserver"; 2 | import { CstChildrenDictionary, IToken, CstNode, ICstVisitor, CstNodeLocation } from "chevrotain" 3 | import { BaseAffVisitor } from "./parser" 4 | import { AFFEvent, AFFItem, AFFFile, AFFMetadata, AFFMetadataEntry, AFFError, AFFValue, WithLocation, AFFTapEvent, AFFValues, AFFHoldEvent, AFFArctapEvent, AFFTimingEvent, AFFArcEvent, AFFTrackId, AFFInt, AFFColorId, AFFArcCurveKind, AFFWord, affArcCurveKinds, affTrackIds, affColorIds, AFFEffect, AFFArcLineKind, affArcLineKinds, AFFCameraEvent, AFFCameraKind, affCameraKinds, AFFSceneControlEvent, AFFSceneControlKind, AFFTimingGroupEvent, AFFNestableItem, AFFTimingGroupKind } from "./types" 5 | import { tokenTypes } from "./lexer"; 6 | 7 | // This pass generate AST from CST. 8 | // It will report errors for things that is valid in CST but not in AST. 9 | // Additional error reporting should check AST instead of CST, so they are not included here 10 | class ToASTVisitor extends BaseAffVisitor implements ICstVisitor { 11 | constructor() { 12 | super() 13 | this.validateVisitor() 14 | } 15 | metadataEntry(ctx: CstChildrenDictionary, errors: AFFError[]): AFFMetadataEntry { 16 | const key = ctx.key[0] as IToken 17 | const data = ctx.data[0] as IToken 18 | return { 19 | key: { data: key.image, location: locationFromToken(key) }, 20 | value: { data: data.image, location: locationFromToken(data) }, 21 | } 22 | } 23 | metadata(ctx: CstChildrenDictionary, errors: AFFError[]): AFFMetadata { 24 | let metadata: AFFMetadata["data"] = new Map() 25 | const entries = ctx.metadataEntry ? (ctx.metadataEntry as CstNode[]).map(node => ({ node, entry: this.visit(node, errors) as AFFMetadataEntry })) : [] 26 | for (const { node, entry } of entries) { 27 | const key = entry.key.data 28 | if (metadata.has(key)) { 29 | let location = metadata.get(key).data.key.location 30 | // error: duplicated entry key 31 | errors.push({ 32 | message: `"${key}" is defined twice in the metadata section`, 33 | location: entry.key.location, 34 | severity: DiagnosticSeverity.Error, 35 | relatedInfo: [{ 36 | message: "Previous defination", 37 | location 38 | }] 39 | }) 40 | } else { 41 | metadata.set(key, { data: entry, location: node.location }) 42 | } 43 | } 44 | return { data: metadata, metaEndLocation: locationFromToken(ctx.metaEnd[0] as IToken) } 45 | } 46 | values(ctx: CstChildrenDictionary, errors: AFFError[]): WithLocation[] { 47 | return ctx.value ? (ctx.value as IToken[]).map((token) => ({ 48 | data: ((token): AFFValue => { 49 | if (token.tokenType === tokenTypes.word) { 50 | return { kind: "word", value: token.image } 51 | } else if (token.tokenType === tokenTypes.int) { 52 | return { kind: "int", value: parseInt(token.image) } 53 | } else if (token.tokenType === tokenTypes.float) { 54 | return { kind: "float", value: parseFloat(token.image), digit: token.image.length - token.image.indexOf(".") - 1 } 55 | } else { 56 | throw new Error(`unknown token type ${token.tokenType}`) 57 | } 58 | })(token), 59 | location: locationFromToken(token) 60 | })) : [] 61 | } 62 | event(ctx: CstChildrenDictionary, errors: AFFError[]): AFFEvent | null { 63 | // type checks 64 | const values = this.visit(ctx.values[0] as CstNode) as WithLocation[] 65 | const valuesLocation = (ctx.values[0] as CstNode).location 66 | const subevents = ctx.subevents ? this.visit(ctx.subevents[0] as CstNode, errors) as WithLocation[] : null 67 | const subeventsLocation = ctx.subevents ? (ctx.subevents[0] as CstNode).location : null 68 | const segment = ctx.segment ? this.visit(ctx.segment[0] as CstNode, errors) as WithLocation[] : null 69 | const segmentLocation = ctx.segment ? (ctx.segment[0] as CstNode).location : null 70 | if (ctx.word) { 71 | const tag = ctx.word[0] as IToken 72 | const tagLocation = locationFromToken(tag) 73 | if (tag.image === "timing") { 74 | return eventTransformer.timing(errors, values, valuesLocation, subevents, subeventsLocation, segment, segmentLocation, tagLocation) 75 | } else if (tag.image === "hold") { 76 | return eventTransformer.hold(errors, values, valuesLocation, subevents, subeventsLocation, segment, segmentLocation, tagLocation) 77 | } else if (tag.image === "arc") { 78 | return eventTransformer.arc(errors, values, valuesLocation, subevents, subeventsLocation, segment, segmentLocation, tagLocation) 79 | } else if (tag.image === "arctap") { 80 | return eventTransformer.arctap(errors, values, valuesLocation, subevents, subeventsLocation, segment, segmentLocation, tagLocation) 81 | } else if (tag.image === "camera") { 82 | return eventTransformer.camera(errors, values, valuesLocation, subevents, subeventsLocation, segment, segmentLocation, tagLocation) 83 | } else if (tag.image === "scenecontrol") { 84 | return eventTransformer.scenecontrol(errors, values, valuesLocation, subevents, subeventsLocation, segment, segmentLocation, tagLocation) 85 | } else if (tag.image === "timinggroup") { 86 | return eventTransformer.timinggroup(errors, values, valuesLocation, subevents, subeventsLocation, segment, segmentLocation, tagLocation) 87 | } else { 88 | // error: unknown tag 89 | errors.push({ 90 | message: `Unknown event type "${tag.image}"`, 91 | location: locationFromToken(tag), 92 | severity: DiagnosticSeverity.Error 93 | }) 94 | return null 95 | } 96 | } else { 97 | return eventTransformer.tap(errors, values, valuesLocation, subevents, subeventsLocation, segment, segmentLocation) 98 | } 99 | } 100 | subevents(ctx: CstChildrenDictionary, errors: AFFError[]): WithLocation[] { 101 | return ctx.event ? (ctx.event as CstNode[]).map(node => ({ 102 | data: this.visit(node, errors) as AFFEvent | null, 103 | location: node.location 104 | })).filter(e => e.data !== null) : [] 105 | } 106 | segment(ctx: CstChildrenDictionary, errors: AFFError[]): WithLocation[] { 107 | return ctx.items ? this.visit(ctx.items[0] as CstNode, errors) : [] 108 | } 109 | item(ctx: CstChildrenDictionary, errors: AFFError[]): WithLocation | null { 110 | let node = ctx.event[0] as CstNode 111 | let event = this.visit(node, errors) as AFFEvent | null 112 | if (event !== null) { 113 | if (event.kind === "arctap") { 114 | //error: arctap should not be items 115 | errors.push({ 116 | message: `Event with type "${event.kind}" should not be used as an item`, 117 | location: event.tagLocation, 118 | severity: DiagnosticSeverity.Error 119 | }) 120 | return null 121 | } 122 | return { data: event, location: node.location } 123 | } 124 | return null 125 | } 126 | items(ctx: CstChildrenDictionary, errors: AFFError[]): WithLocation[] { 127 | return ctx.item ? (ctx.item as CstNode[]).map(node => this.visit(node, errors) as WithLocation | null).filter(e => e !== null) : [] 128 | } 129 | aff(ctx: CstChildrenDictionary, errors: AFFError[]): AFFFile { 130 | const metadataNode = ctx.metadata[0] as CstNode 131 | const metadata = this.visit(metadataNode, errors) as AFFMetadata 132 | const items = this.visit(ctx.items[0] as CstNode, errors) as WithLocation[] 133 | return { metadata: { data: metadata, location: metadataNode.location }, items } 134 | } 135 | } 136 | 137 | const toASTVisitor = new ToASTVisitor() 138 | 139 | export const affToAST = (aff) => { 140 | let errors: AFFError[] = [] 141 | const ast = toASTVisitor.visit(aff, errors) as AFFFile 142 | return { ast, errors } 143 | } 144 | 145 | // helpers 146 | const locationFromToken = (token: IToken): CstNodeLocation => { 147 | const { startColumn, startLine, startOffset, endColumn, endLine, endOffset } = token 148 | return { startColumn, startLine, startOffset, endColumn, endLine, endOffset } 149 | } 150 | 151 | const rejectSubevent = (errors: AFFError[], kind: string, subevents: WithLocation[] | null, subeventsLocation: CstNodeLocation | null) => { 152 | if (subevents !== null) { 153 | // error: unexpected subevent 154 | errors.push({ 155 | message: `Event with type "${kind}" should not have subevents`, 156 | location: subeventsLocation, 157 | severity: DiagnosticSeverity.Error, 158 | }) 159 | } 160 | } 161 | 162 | const rejectSegment = (errors: AFFError[], kind: string, segment: WithLocation[] | null, segmentLocation: CstNodeLocation | null) => { 163 | if (segment !== null) { 164 | // error: unexpected subevent 165 | errors.push({ 166 | message: `Event with type "${kind}" should not have segment`, 167 | location: segmentLocation, 168 | severity: DiagnosticSeverity.Error, 169 | }) 170 | } 171 | } 172 | 173 | const ensureValuesCount = (errors: AFFError[], kind: string, count: number, values: WithLocation[], valuesLocation: CstNodeLocation): boolean => { 174 | if (values.length < count) { 175 | // error: value count mismatch 176 | errors.push({ 177 | message: `Event with type "${kind}" should have at least ${count} field(s) instead of ${values.length} field(s)`, 178 | location: valuesLocation, 179 | severity: DiagnosticSeverity.Error, 180 | }) 181 | return false 182 | } 183 | return true 184 | } 185 | 186 | const limitValuesCount = (errors: AFFError[], kind: string, count: number, values: WithLocation[], valuesLocation: CstNodeLocation): boolean => { 187 | if (values.length > count) { 188 | // error: value count mismatch 189 | errors.push({ 190 | message: `Event with type "${kind}" should have at most ${count} field(s) instead of ${values.length} field(s)`, 191 | location: valuesLocation, 192 | severity: DiagnosticSeverity.Error, 193 | }) 194 | return false 195 | } 196 | return true 197 | } 198 | 199 | const checkValuesCount = (errors: AFFError[], kind: string, count: number, values: WithLocation[], valuesLocation: CstNodeLocation): boolean => { 200 | if (values.length !== count) { 201 | // error: value count mismatch 202 | errors.push({ 203 | message: `Event with type "${kind}" should have ${count} field(s) instead of ${values.length} field(s)`, 204 | location: valuesLocation, 205 | severity: DiagnosticSeverity.Error, 206 | }) 207 | return false 208 | } 209 | return true 210 | } 211 | 212 | const checkValueType = ( 213 | errors: AFFError[], 214 | eventKind: string, 215 | fieldName: string, 216 | kind: T, 217 | values: WithLocation[], 218 | id: number 219 | ): WithLocation | null => { 220 | const value = values[id] 221 | if (value.data.kind !== kind) { 222 | // error: value type mismatch 223 | errors.push({ 224 | message: `The value in the "${fieldName}" field of event with type "${eventKind}" should be "${kind}" instead of "${value.data.kind}"`, 225 | location: values[id].location, 226 | severity: DiagnosticSeverity.Error, 227 | }) 228 | return null 229 | } else { 230 | return value as WithLocation 231 | } 232 | } 233 | 234 | const eventTransformer = { 235 | tap: ( 236 | errors: AFFError[], 237 | values: WithLocation[], 238 | valuesLocation: CstNodeLocation, 239 | subevents: WithLocation[] | null, 240 | subeventsLocation: CstNodeLocation | null, 241 | segment: WithLocation[] | null, 242 | segmentLocation: CstNodeLocation | null, 243 | ): AFFTapEvent | null => { 244 | rejectSubevent(errors, "tap", subevents, subeventsLocation) 245 | rejectSegment(errors, "tap", segment, segmentLocation) 246 | if (!checkValuesCount(errors, "tap", 2, values, valuesLocation)) { 247 | return null 248 | } 249 | const time = checkValueType(errors, "tap", "time", "int", values, 0) 250 | const rawTrackId = checkValueType(errors, "tap", "track-id", "int", values, 1) 251 | const trackId = parseValue.trackId(errors, "tap", "track-id", rawTrackId) 252 | if (time === null || trackId === null) { 253 | return null 254 | } 255 | return { kind: "tap", time, trackId: trackId } 256 | }, 257 | hold: ( 258 | errors: AFFError[], 259 | values: WithLocation[], 260 | valuesLocation: CstNodeLocation, 261 | subevents: WithLocation[] | null, 262 | subeventsLocation: CstNodeLocation | null, 263 | segment: WithLocation[] | null, 264 | segmentLocation: CstNodeLocation | null, 265 | tagLocation: CstNodeLocation, 266 | ): AFFHoldEvent | null => { 267 | rejectSubevent(errors, "hold", subevents, subeventsLocation) 268 | rejectSegment(errors, "hold", segment, segmentLocation) 269 | if (!checkValuesCount(errors, "hold", 3, values, valuesLocation)) { 270 | return null 271 | } 272 | const start = checkValueType(errors, "hold", "start", "int", values, 0) 273 | const end = checkValueType(errors, "hold", "end", "int", values, 1) 274 | const rawTrackId = checkValueType(errors, "hold", "track-id", "int", values, 2) 275 | const trackId = parseValue.trackId(errors, "hold", "track-id", rawTrackId) 276 | if (start === null || end === null || trackId === null) { 277 | return null 278 | } 279 | return { kind: "hold", start, end, trackId: trackId, tagLocation } 280 | }, 281 | arctap: ( 282 | errors: AFFError[], 283 | values: WithLocation[], 284 | valuesLocation: CstNodeLocation, 285 | subevents: WithLocation[] | null, 286 | subeventsLocation: CstNodeLocation | null, 287 | segment: WithLocation[] | null, 288 | segmentLocation: CstNodeLocation | null, 289 | tagLocation: CstNodeLocation, 290 | ): AFFArctapEvent | null => { 291 | rejectSubevent(errors, "arctap", subevents, subeventsLocation) 292 | rejectSegment(errors, "arctap", segment, segmentLocation) 293 | if (!checkValuesCount(errors, "arctap", 1, values, valuesLocation)) { 294 | return null 295 | } 296 | const time = checkValueType(errors, "arctap", "time", "int", values, 0) 297 | if (time === null) { 298 | return null 299 | } 300 | return { kind: "arctap", time, tagLocation } 301 | }, 302 | timing: ( 303 | errors: AFFError[], 304 | values: WithLocation[], 305 | valuesLocation: CstNodeLocation, 306 | subevents: WithLocation[] | null, 307 | subeventsLocation: CstNodeLocation | null, 308 | segment: WithLocation[] | null, 309 | segmentLocation: CstNodeLocation | null, 310 | tagLocation: CstNodeLocation, 311 | ): AFFTimingEvent | null => { 312 | rejectSubevent(errors, "timing", subevents, subeventsLocation) 313 | rejectSegment(errors, "timing", segment, segmentLocation) 314 | if (!checkValuesCount(errors, "timing", 3, values, valuesLocation)) { 315 | return null 316 | } 317 | const time = checkValueType(errors, "timing", "time", "int", values, 0) 318 | const bpm = checkValueType(errors, "timing", "bpm", "float", values, 1) 319 | const measure = checkValueType(errors, "timing", "measure", "float", values, 2) 320 | if (time === null || bpm === null || measure === null) { 321 | return null 322 | } 323 | return { kind: "timing", time, bpm, measure, tagLocation } 324 | }, 325 | arc: ( 326 | errors: AFFError[], 327 | values: WithLocation[], 328 | valuesLocation: CstNodeLocation, 329 | subevents: WithLocation[] | null, 330 | subeventsLocation: CstNodeLocation | null, 331 | segment: WithLocation[] | null, 332 | segmentLocation: CstNodeLocation | null, 333 | tagLocation: CstNodeLocation, 334 | ): AFFArcEvent | null => { 335 | rejectSegment(errors, "arc", segment, segmentLocation) 336 | if (!checkValuesCount(errors, "arc", 10, values, valuesLocation)) { 337 | return null 338 | } 339 | const start = checkValueType(errors, "arc", "start", "int", values, 0) 340 | const end = checkValueType(errors, "arc", "end", "int", values, 1) 341 | const xStart = checkValueType(errors, "arc", "x-start", "float", values, 2) 342 | const xEnd = checkValueType(errors, "arc", "x-end", "float", values, 3) 343 | const rawCurveKind = checkValueType(errors, "arc", "curve-kind", "word", values, 4) 344 | const curveKind = parseValue.arcCurveKind(errors, "arc", "curve-kind", rawCurveKind) 345 | const yStart = checkValueType(errors, "arc", "y-start", "float", values, 5) 346 | const yEnd = checkValueType(errors, "arc", "y-end", "float", values, 6) 347 | const rawColorId = checkValueType(errors, "arc", "color-id", "int", values, 7) 348 | const colorId = parseValue.colorId(errors, "arc", "color-id", rawColorId) 349 | const rawEffect = checkValueType(errors, "arc", "effect", "word", values, 8) 350 | const effect = parseValue.effect(errors, "arc", "effect", rawEffect) 351 | const rawLineKind = checkValueType(errors, "arc", "line-kind", "word", values, 9) 352 | const lineKind = parseValue.arcLineKind(errors, "arc", "line-kind", rawLineKind) 353 | if (start === null || end === null || 354 | xStart === null || xEnd === null || curveKind === null || 355 | yStart === null || yEnd === null || colorId === null || 356 | effect === null || lineKind === null) { 357 | return null 358 | } 359 | return { 360 | kind: "arc", start, end, xStart, xEnd, curveKind, yStart, yEnd, colorId, effect, lineKind, 361 | arctaps: subevents ? transformArcSubevents(errors, subevents, subeventsLocation) : undefined, tagLocation 362 | } 363 | }, 364 | camera: ( 365 | errors: AFFError[], 366 | values: WithLocation[], 367 | valuesLocation: CstNodeLocation, 368 | subevents: WithLocation[] | null, 369 | subeventsLocation: CstNodeLocation | null, 370 | segment: WithLocation[] | null, 371 | segmentLocation: CstNodeLocation | null, 372 | tagLocation: CstNodeLocation, 373 | ): AFFCameraEvent | null => { 374 | rejectSubevent(errors, "camera", subevents, subeventsLocation) 375 | rejectSegment(errors, "camera", segment, segmentLocation) 376 | if (!checkValuesCount(errors, "camera", 9, values, valuesLocation)) { 377 | return null 378 | } 379 | const time = checkValueType(errors, "camera", "time", "int", values, 0) 380 | const translationX = checkValueType(errors, "camera", "translation-x", "float", values, 1) 381 | const translationY = checkValueType(errors, "camera", "translation-y", "float", values, 2) 382 | const translationZ = checkValueType(errors, "camera", "translation-z", "float", values, 3) 383 | const rotationX = checkValueType(errors, "camera", "rotation-x", "float", values, 4) 384 | const rotationY = checkValueType(errors, "camera", "rotation-y", "float", values, 5) 385 | const rotationZ = checkValueType(errors, "camera", "rotation-z", "float", values, 6) 386 | const rawCameraKind = checkValueType(errors, "camera", "camera-kind", "word", values, 7) 387 | const cameraKind = parseValue.cameraKind(errors, "camera", "camera-kind", rawCameraKind) 388 | const duration = checkValueType(errors, "camera", "time", "int", values, 8) 389 | if (time === null || 390 | translationX === null || translationY === null || translationZ === null || 391 | rotationX === null || rotationY === null || rotationZ === null || 392 | cameraKind === null || duration === null 393 | ) { 394 | return null 395 | } 396 | return { 397 | kind: "camera", time, translationX, translationY, translationZ, rotationX, rotationY, rotationZ, cameraKind, duration, 398 | tagLocation 399 | } 400 | }, 401 | scenecontrol: ( 402 | errors: AFFError[], 403 | values: WithLocation[], 404 | valuesLocation: CstNodeLocation, 405 | subevents: WithLocation[] | null, 406 | subeventsLocation: CstNodeLocation | null, 407 | segment: WithLocation[] | null, 408 | segmentLocation: CstNodeLocation | null, 409 | tagLocation: CstNodeLocation, 410 | ): AFFSceneControlEvent | null => { 411 | rejectSubevent(errors, "scenecontrol", subevents, subeventsLocation) 412 | rejectSegment(errors, "scenecontrol", segment, segmentLocation) 413 | if (!ensureValuesCount(errors, "scenecontrol", 2, values, valuesLocation)) { 414 | return null 415 | } 416 | const time = checkValueType(errors, "scenecontrol", "time", "int", values, 0) 417 | const rawSceneControlKind = checkValueType(errors, "scenecontrol", "scenecontrol-kind", "word", values, 1) 418 | const sceneControlKind = parseValue.sceneControlKind(errors, "scenecontrol", "scenecontrol-kind", rawSceneControlKind) 419 | const additionalValues = sceneControlKind === null ? null : { 420 | data: values.slice(2), 421 | location: { 422 | startOffset: sceneControlKind.location.endOffset + 1, 423 | startLine: sceneControlKind.location.endLine, 424 | startColumn: sceneControlKind.location.endColumn + 1, 425 | endOffset: valuesLocation.endOffset, 426 | endLine: valuesLocation.endLine, 427 | endColumn: valuesLocation.endColumn, 428 | } 429 | } 430 | if (time === null || sceneControlKind === null) { 431 | return null 432 | } 433 | return { 434 | kind: "scenecontrol", time, sceneControlKind, tagLocation, values: additionalValues 435 | } 436 | }, 437 | timinggroup: ( 438 | errors: AFFError[], 439 | values: WithLocation[], 440 | valuesLocation: CstNodeLocation, 441 | subevents: WithLocation[] | null, 442 | subeventsLocation: CstNodeLocation | null, 443 | segment: WithLocation[] | null, 444 | segmentLocation: CstNodeLocation | null, 445 | tagLocation: CstNodeLocation, 446 | ): AFFTimingGroupEvent | null => { 447 | rejectSubevent(errors, "timinggroup", subevents, subeventsLocation) 448 | if (!limitValuesCount(errors, "timinggroup", 1, values, valuesLocation)) { 449 | return null 450 | } 451 | const rawTimingGroupKind = values.length > 0 ? checkValueType(errors, "timinggroup", "timinggroup-kind", "word", values, 0) : null 452 | const maybeTimingGroupKind = parseValue.timingGroupKind(errors, "timinggroup", "timinggroup-kind", rawTimingGroupKind) 453 | const timingGroupKind = maybeTimingGroupKind ?? { 454 | location: valuesLocation, 455 | data: { 456 | kind: "timinggroup-kind", 457 | value: "" 458 | } 459 | } 460 | return { 461 | kind: "timinggroup", items: selectNestedItems(errors, segment, segmentLocation), tagLocation, timingGroupAttribute: timingGroupKind 462 | } 463 | } 464 | } 465 | 466 | const transformArcSubevents = ( 467 | errors: AFFError[], 468 | subevents: WithLocation[], 469 | subeventsLocation: CstNodeLocation 470 | ): WithLocation[]> => { 471 | let arctaps: WithLocation[] = [] 472 | for (const { location, data: event } of subevents) { 473 | if (event.kind !== "arctap") { 474 | errors.push({ 475 | message: `Type of subevent of event with type "arc" should be "arctap" instead of "${event.kind}"`, 476 | location: location, 477 | severity: DiagnosticSeverity.Error, 478 | }) 479 | } else { 480 | arctaps.push({ location, data: event }) 481 | } 482 | } 483 | return { data: arctaps, location: subeventsLocation } 484 | } 485 | 486 | const selectNestedItems = ( 487 | errors: AFFError[], 488 | segment: WithLocation[], 489 | segmentLocation: CstNodeLocation 490 | ): WithLocation[]> => { 491 | let items: WithLocation[] = [] 492 | for (const { location, data: item } of segment) { 493 | if (item.kind === "timinggroup") { 494 | errors.push({ 495 | message: `Item of type "${item.kind}" cannot be nested in timinggroup`, 496 | location: location, 497 | severity: DiagnosticSeverity.Error, 498 | }) 499 | } else { 500 | items.push({ location, data: item } as WithLocation) 501 | } 502 | } 503 | return { data: items, location: segmentLocation } 504 | } 505 | 506 | const parseValue = { 507 | trackId: (errors: AFFError[], eventKind: string, fieldName: string, int: WithLocation | null): WithLocation => { 508 | if (int) { 509 | const { data, location } = int 510 | const intValue = data.value 511 | if (!Number.isInteger(intValue)) { 512 | throw new Error(`value in AFFInt(${intValue}) is not int`) 513 | } 514 | if (intValue < 0 || intValue > 5) { 515 | errors.push({ 516 | message: `The value in the "${fieldName}" field of event with type "${eventKind}" should be one of ${[...affTrackIds.values()].join()}`, 517 | location, 518 | severity: DiagnosticSeverity.Error, 519 | }) 520 | return null 521 | } 522 | return { data: { kind: "track-id", value: intValue } as AFFTrackId, location } 523 | } else { 524 | return null 525 | } 526 | }, 527 | colorId: (errors: AFFError[], eventKind: string, fieldName: string, int: WithLocation | null): WithLocation => { 528 | if (int) { 529 | const { data, location } = int 530 | const intValue = data.value 531 | if (!Number.isInteger(intValue)) { 532 | throw new Error(`value in AFFInt(${intValue}) is not int`) 533 | } 534 | if (intValue < 0 || intValue > 3) { 535 | errors.push({ 536 | message: `The value in the "${fieldName}" field of event with type "${eventKind}" should be one of ${[...affColorIds.values()].join()}`, 537 | location, 538 | severity: DiagnosticSeverity.Error, 539 | }) 540 | return null 541 | } 542 | return { data: { kind: "color-id", value: intValue } as AFFColorId, location } 543 | } else { 544 | return null 545 | } 546 | }, 547 | arcCurveKind: (errors: AFFError[], eventKind: string, fieldName: string, word: WithLocation | null): WithLocation => { 548 | if (word) { 549 | const { data, location } = word 550 | const wordValue = data.value 551 | if (!affArcCurveKinds.has(wordValue)) { 552 | errors.push({ 553 | message: `The value in the "${fieldName}" field of event with type "${eventKind}" should be one of ${[...affArcCurveKinds.values()].join()}`, 554 | location, 555 | severity: DiagnosticSeverity.Error, 556 | }) 557 | return null 558 | } 559 | return { data: { kind: "arc-curve-kind", value: wordValue } as AFFArcCurveKind, location } 560 | } else { 561 | return null 562 | } 563 | }, 564 | effect: (_errors: AFFError[], _eventKind: string, _fieldName: string, word: WithLocation | null): WithLocation => { 565 | if (word) { 566 | const { data, location } = word 567 | const wordValue = data.value 568 | return { data: { kind: "effect", value: wordValue } as AFFEffect, location } 569 | } else { 570 | return null 571 | } 572 | }, 573 | arcLineKind: (errors: AFFError[], eventKind: string, fieldName: string, word: WithLocation | null): WithLocation => { 574 | if (word) { 575 | const { data, location } = word 576 | const wordValue = data.value 577 | if (!affArcLineKinds.has(wordValue)) { 578 | errors.push({ 579 | message: `The value in the "${fieldName}" field of event with type "${eventKind}" should be one of ${[...affArcLineKinds.values()].join()}`, 580 | location, 581 | severity: DiagnosticSeverity.Error, 582 | }) 583 | return null 584 | } 585 | return { data: { kind: "arc-line-kind", value: wordValue } as AFFArcLineKind, location } 586 | } else { 587 | return null 588 | } 589 | }, 590 | cameraKind: (errors: AFFError[], eventKind: string, fieldName: string, word: WithLocation | null): WithLocation => { 591 | if (word) { 592 | const { data, location } = word 593 | const wordValue = data.value 594 | if (!affCameraKinds.has(wordValue)) { 595 | errors.push({ 596 | message: `The value in the "${fieldName}" field of event with type "${eventKind}" should be one of ${[...affCameraKinds.values()].join()}`, 597 | location, 598 | severity: DiagnosticSeverity.Error, 599 | }) 600 | return null 601 | } 602 | return { data: { kind: "camera-kind", value: wordValue } as AFFCameraKind, location } 603 | } else { 604 | return null 605 | } 606 | }, 607 | sceneControlKind: (errors: AFFError[], eventKind: string, fieldName: string, word: WithLocation | null): WithLocation => { 608 | if (word) { 609 | const { data, location } = word 610 | const wordValue = data.value 611 | return { data: { kind: "scenecontrol-kind", value: wordValue } as AFFSceneControlKind, location } 612 | } else { 613 | return null 614 | } 615 | }, 616 | timingGroupKind: (errors: AFFError[], eventKind: string, fieldName: string, word: WithLocation | null): WithLocation => { 617 | if (word) { 618 | const { data, location } = word 619 | const wordValue = data.value 620 | return { data: { kind: "timinggroup-kind", value: wordValue } as AFFTimingGroupKind, location } 621 | } else { 622 | return null 623 | } 624 | } 625 | } -------------------------------------------------------------------------------- /server/src/types.ts: -------------------------------------------------------------------------------- 1 | import { CstNodeLocation } from "chevrotain" 2 | import { DiagnosticSeverity } from "vscode-languageserver"; 3 | 4 | export interface WithLocation { 5 | data: T, 6 | location: CstNodeLocation, 7 | } 8 | export interface AFFTimingEvent { 9 | kind: "timing", 10 | tagLocation: CstNodeLocation, 11 | time: WithLocation, 12 | bpm: WithLocation, 13 | measure: WithLocation, 14 | } 15 | 16 | export interface AFFTapEvent { 17 | kind: "tap", 18 | time: WithLocation, 19 | trackId: WithLocation, 20 | } 21 | 22 | export interface AFFHoldEvent { 23 | kind: "hold", 24 | tagLocation: CstNodeLocation, 25 | start: WithLocation, 26 | end: WithLocation, 27 | trackId: WithLocation, 28 | } 29 | 30 | export interface AFFArcEvent { 31 | kind: "arc", 32 | tagLocation: CstNodeLocation, 33 | start: WithLocation, 34 | end: WithLocation, 35 | xStart: WithLocation, 36 | xEnd: WithLocation, 37 | curveKind: WithLocation, 38 | yStart: WithLocation, 39 | yEnd: WithLocation, 40 | colorId: WithLocation, 41 | effect: WithLocation, 42 | lineKind: WithLocation, 43 | arctaps?: WithLocation[]>, 44 | } 45 | 46 | export interface AFFArctapEvent { 47 | kind: "arctap", 48 | tagLocation: CstNodeLocation, 49 | time: WithLocation, 50 | } 51 | 52 | export interface AFFCameraEvent { 53 | kind: "camera" 54 | tagLocation: CstNodeLocation, 55 | time: WithLocation, 56 | translationX: WithLocation, 57 | translationY: WithLocation, 58 | translationZ: WithLocation, 59 | rotationX: WithLocation, 60 | rotationY: WithLocation, 61 | rotationZ: WithLocation, 62 | cameraKind: WithLocation 63 | duration: WithLocation 64 | } 65 | 66 | export interface AFFSceneControlEvent { 67 | kind: "scenecontrol", 68 | tagLocation: CstNodeLocation, 69 | time: WithLocation, 70 | sceneControlKind: WithLocation 71 | values: WithLocation[]> 72 | } 73 | 74 | export interface AFFTimingGroupEvent { 75 | kind: "timinggroup", 76 | timingGroupAttribute: WithLocation, 77 | tagLocation: CstNodeLocation, 78 | items: WithLocation[]>, 79 | } 80 | 81 | export type AFFTrackItem = AFFTapEvent | AFFHoldEvent 82 | export type AFFNestableItem = AFFTimingEvent | AFFTrackItem | AFFArcEvent | AFFSceneControlEvent | AFFCameraEvent 83 | export type AFFItem = AFFNestableItem | AFFCameraEvent | AFFTimingGroupEvent 84 | export type AFFEvent = AFFItem | AFFArctapEvent 85 | 86 | export interface AFFInt { 87 | kind: "int", 88 | value: number, 89 | } 90 | 91 | export interface AFFFloat { 92 | kind: "float", 93 | value: number, 94 | digit: number, 95 | } 96 | 97 | export interface AFFWord { 98 | kind: "word", 99 | value: string 100 | } 101 | 102 | export type AFFValues = { 103 | int: AFFInt, 104 | float: AFFFloat, 105 | word: AFFWord, 106 | } 107 | export type AFFValue = AFFValues[keyof AFFValues] 108 | 109 | export interface AFFTrackId { 110 | kind: "track-id", 111 | value: 0 | 1 | 2 | 3 | 4 | 5, 112 | } 113 | export type AFFTrackIdValue = AFFTrackId["value"] 114 | export const affTrackIds = new Set([0, 1, 2, 3, 4, 5]) 115 | 116 | export interface AFFColorId { 117 | kind: "color-id", 118 | value: 0 | 1 | 2 | 3 | 4, 119 | } 120 | export type AFFColorIdValue = AFFColorId["value"] 121 | export const affColorIds = new Set([0, 1, 2, 3]) 122 | 123 | export interface AFFEffect { 124 | kind: "effect", 125 | value: string 126 | } 127 | 128 | export interface AFFArcCurveKind { 129 | kind: "arc-curve-kind", 130 | value: "b" | "s" | "si" | "so" | "sisi" | "siso" | "soso" | "sosi" 131 | } 132 | export const affArcCurveKinds = new Set(["b", "s", "si", "so", "sisi", "siso", "soso", "sosi"]) 133 | 134 | export interface AFFCameraKind { 135 | kind: "camera-kind", 136 | value: "l" | "s" | "qi" | "qo" | "reset" 137 | } 138 | export const affCameraKinds = new Set(["l", "s", "qi", "qo", "reset"]) 139 | 140 | export interface AFFSceneControlKind { 141 | kind: "scenecontrol-kind", 142 | value: string 143 | } 144 | 145 | export interface AFFTimingGroupKind { 146 | kind: "timinggroup-kind", 147 | value: string 148 | } 149 | 150 | export interface AFFArcLineKind { 151 | kind: "arc-line-kind", 152 | value: string 153 | } 154 | export const affArcLineKinds = new Set(["true", "false", "designant"]) 155 | export const isLine = (data: AFFArcLineKind) => data.value !== "false" 156 | 157 | export interface AFFMetadataEntry { 158 | key: WithLocation, 159 | value: WithLocation, 160 | } 161 | 162 | export interface AFFMetadata { 163 | data: Map> 164 | metaEndLocation: CstNodeLocation 165 | } 166 | 167 | export interface AFFFile { 168 | metadata: WithLocation, 169 | items: WithLocation[], 170 | } 171 | 172 | export interface AFFErrorRelatedInfo { 173 | message: string, 174 | location: CstNodeLocation, 175 | } 176 | 177 | export interface AFFError { 178 | message: string, 179 | location: CstNodeLocation, 180 | severity: DiagnosticSeverity, 181 | relatedInfo?: AFFErrorRelatedInfo[], 182 | } 183 | 184 | export type AFFChecker = (file: AFFFile, errors: AFFError[]) => void -------------------------------------------------------------------------------- /server/src/util/associated-data.ts: -------------------------------------------------------------------------------- 1 | export type DataGenerator = (input: K) => V 2 | 3 | export class AssociatedDataMap { 4 | map = new WeakMap(); 5 | gen: DataGenerator; 6 | constructor(gen: DataGenerator) { 7 | this.map = new WeakMap() 8 | this.gen = gen 9 | } 10 | get(key:K){ 11 | if(this.map.has(key)){ 12 | return this.map.get(key) 13 | }else{ 14 | const result=this.gen(key) 15 | this.map.set(key,result) 16 | return result 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/src/util/misc.ts: -------------------------------------------------------------------------------- 1 | // first item that is not less than value 2 | export const lowerBound = (array: T[], value: U, comparer: (a: T, b: U) => number): number => { 3 | let start = 0, count = array.length 4 | while (count > 0) { 5 | const step = (count - count % 2) / 2 6 | const pos = start + step 7 | if (comparer(array[start + step], value) < 0) { 8 | start = pos + 1 9 | count -= step + 1 10 | } else { 11 | count = step 12 | } 13 | } 14 | return start 15 | } 16 | 17 | // first item that is greater than value 18 | export const upperBound = (array: T[], value: U, comparer: (a: T, b: U) => number): number => { 19 | let start = 0, count = array.length 20 | while (count > 0) { 21 | const step = (count - count % 2) / 2 22 | const pos = start + step 23 | if (comparer(array[start + step], value) <= 0) { 24 | start = pos + 1 25 | count -= step + 1 26 | } else { 27 | count = step 28 | } 29 | } 30 | return start 31 | } -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es6", 6 | "outDir": "out", 7 | "rootDir": "src", 8 | "lib": ["es6"], 9 | "sourceMap": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", ".vscode-test"] 13 | } -------------------------------------------------------------------------------- /snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Initialize the aff file": { 3 | "prefix": "init", 4 | "body": [ 5 | "AudioOffset:${3:0}", 6 | "-", 7 | "timing(0,${1:100.00},${2:4.00});" 8 | ] 9 | }, 10 | "Tap": { 11 | "prefix": "tap", 12 | "body": [ 13 | "(${1:0},${2:1});" 14 | ] 15 | }, 16 | "Hold": { 17 | "prefix": "hold", 18 | "body": [ 19 | "hold(${1:0},${2:100},${3:1});" 20 | ] 21 | }, 22 | "Arctap": { 23 | "prefix": "arctap", 24 | "body": [ 25 | "arctap(${1:0})" 26 | ] 27 | }, 28 | "Arc": { 29 | "prefix": "arc", 30 | "body": [ 31 | "arc(${1:0},${2:100},${3:0.00},${4:0.00},${5:s},${6:0.00},${7:0.00},${8:0},none,${9:false});" 32 | ] 33 | }, 34 | "timing": { 35 | "prefix": "timing", 36 | "body": [ 37 | "timing(${1:0},${2:100.00},${3:4.00});" 38 | ] 39 | }, 40 | "timinggroup": { 41 | "prefix": "timinggroup", 42 | "body": [ 43 | "timinggroup(){", 44 | " timing(0,${1:100.00},${2:4.00});", 45 | "};" 46 | ] 47 | } 48 | } -------------------------------------------------------------------------------- /syntaxes/arcaea-aff.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "Arcaea File Format", 4 | "patterns": [ 5 | { 6 | "include": "#divider" 7 | }, 8 | { 9 | "include": "#metadata" 10 | }, 11 | { 12 | "include": "#event" 13 | }, 14 | { 15 | "include": "#list" 16 | }, 17 | { 18 | "include": "#segment" 19 | }, 20 | { 21 | "include": "#semicolon" 22 | } 23 | ], 24 | "repository": { 25 | "divider": { 26 | "match": "-", 27 | "name": "punctuation.separator.divider.arcaea-aff" 28 | }, 29 | "metadata": { 30 | "match": "([^:]*)(:)(.*)", 31 | "name": "meta.metadata.arcaea-aff", 32 | "captures": { 33 | "1": { 34 | "name": "support.type.property-name.arcaea-aff" 35 | }, 36 | "2": { 37 | "name": "punctuation.separator.dictionary.key-value.arcaea-aff" 38 | }, 39 | "3": { 40 | "name": "meta.dictionary.value.arcaea-aff", 41 | "patterns": [ 42 | { 43 | "include": "#value" 44 | } 45 | ] 46 | } 47 | } 48 | }, 49 | "event": { 50 | "begin": "([a-z]*)\\s*(\\()", 51 | "beginCaptures": { 52 | "1": { 53 | "name":"keyword.other.tag.arcaea-aff" 54 | }, 55 | "2": { 56 | "name": "punctuation.section.tuple.begin.paren.arcaea-aff" 57 | } 58 | }, 59 | "end": "(\\))", 60 | "endCaptures": { 61 | "1": { 62 | "name": "punctuation.section.tuple.end.paren.arcaea-aff" 63 | } 64 | }, 65 | "contentName": "meta.tuple.content.arcaea-aff", 66 | "patterns": [ 67 | { 68 | "include": "#value" 69 | }, 70 | { 71 | "include": "#comma" 72 | } 73 | ] 74 | }, 75 | "list": { 76 | "begin": "(\\[)", 77 | "beginCaptures": { 78 | "1": { 79 | "name": "punctuation.section.list.begin.bracket.arcaea-aff" 80 | } 81 | }, 82 | "end": "(\\])", 83 | "endCaptures": { 84 | "1": { 85 | "name": "punctuation.section.list.end.bracket.arcaea-aff" 86 | } 87 | }, 88 | "contentName": "meta.list.content.arcaea-aff", 89 | "patterns": [ 90 | { 91 | "include": "#event" 92 | }, 93 | { 94 | "include": "#comma" 95 | } 96 | ] 97 | }, 98 | "segment": { 99 | "begin": "(\\{)", 100 | "beginCaptures": { 101 | "1": { 102 | "name": "punctuation.section.block.begin.brace.arcaea-aff" 103 | } 104 | }, 105 | "end": "(\\})", 106 | "endCaptures": { 107 | "1": { 108 | "name": "punctuation.section.block.end.brace.arcaea-aff" 109 | } 110 | }, 111 | "contentName": "meta.block.content.arcaea-aff", 112 | "patterns": [ 113 | { 114 | "include": "#event" 115 | }, 116 | { 117 | "include": "#list" 118 | }, 119 | { 120 | "include": "#segment" 121 | }, 122 | { 123 | "include": "#semicolon" 124 | } 125 | ] 126 | }, 127 | "semicolon": { 128 | "match": ";", 129 | "name": "punctuation.terminator.semicolon.arcaea-aff" 130 | }, 131 | "comma": { 132 | "match": ",", 133 | "name": "punctuation.separator.comma.arcaea-aff" 134 | }, 135 | "value": { 136 | "patterns": [ 137 | { 138 | "include": "#float" 139 | }, 140 | { 141 | "include": "#integer" 142 | }, 143 | { 144 | "include": "#enum" 145 | } 146 | ] 147 | }, 148 | "float": { 149 | "match": "\\b-?[0-9]+\\.[0-9]+\\b", 150 | "name": "constant.numeric.float.arcaea-aff" 151 | }, 152 | "integer": { 153 | "match": "\\b-?(?:0|[1-9][0-9]*)\\b", 154 | "name": "constant.numeric.integer.arcaea-aff" 155 | }, 156 | "enum": { 157 | "match": "\\b[a-zA-Z][a-zA-Z0-9_]*\\b", 158 | "name": "constant.language.enum.arcaea-aff" 159 | } 160 | }, 161 | "scopeName": "source.arcaea-aff" 162 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es6", 6 | "outDir": "out", 7 | "rootDir": "src", 8 | "lib": [ "es6" ], 9 | "sourceMap": true 10 | }, 11 | "include": [ 12 | "src" 13 | ], 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode-test" 17 | ], 18 | "references": [ 19 | { "path": "./client" }, 20 | { "path": "./server" } 21 | ] 22 | } --------------------------------------------------------------------------------