├── .gitattributes ├── .github ├── actions │ └── prepare-pr │ │ ├── index.ts │ │ ├── package.json │ │ └── tsconfig.json └── workflows │ ├── build.yml │ ├── github-pages.yml │ ├── release.yml │ └── validation.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .vscode ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CONTRIBUTE.md ├── LICENSE ├── README.md ├── mock ├── modpack.zip ├── mods │ ├── dummy-mod.jar │ ├── fabric-sample-2.jar │ ├── fabric-sample.jar │ ├── sample-ccc-mod.jar │ ├── sample-mod-1.13-no-deps.jar │ ├── sample-mod-1.13.jar │ ├── sample-mod.jar │ ├── sample-mod.litemod │ ├── sample-nei-mod.jar │ └── tweak-class.jar ├── options.txt ├── pack.png ├── resourcepacks │ ├── 1.14.4.zip │ ├── empty-resourcepack.zip │ ├── sample-resourcepack.zip │ └── sample-resourcepack │ │ ├── pack.mcmeta │ │ └── pack.png ├── sample-forge-new.html ├── sample-forge.html ├── saves │ ├── 1.12.2 │ │ ├── block.txt │ │ ├── data.txt │ │ ├── mock.txt │ │ └── region │ │ │ └── r.0.0.mca │ ├── 1.14.4 │ │ ├── mock.txt │ │ └── region │ │ │ └── r.0.0.mca │ ├── 1.16.4 │ │ ├── mock.txt │ │ └── region │ │ │ └── r.0.0.mca │ ├── 1.16.5 │ │ └── region │ │ │ └── r.0.0.mca │ ├── 1.19.3 │ │ └── region │ │ │ └── r.0.0.mca │ ├── sample-map.zip │ └── sample-map │ │ ├── icon.png │ │ ├── level.dat │ │ ├── playerdata │ │ └── abf81fe9-9f0d-4948-a909-7721a8198ac4.dat │ │ └── region │ │ └── r.0.0.mca ├── servers.dat └── versions │ ├── 1.14.4-forge-28.0.47 │ └── 1.14.4-forge-28.0.47.json │ ├── 1.14.4 │ └── 1.14.4.json │ ├── 1.17.1-forge-37.0.97 │ └── 1.17.1-forge-37.0.97.json │ ├── 1.17.1 │ └── 1.17.1.json │ ├── 1.7.10-Forge10.13.3.1400-1.7.10 │ └── 1.7.10-Forge10.13.3.1400-1.7.10.json │ ├── 1.7.10 │ └── 1.7.10.json │ ├── mock │ └── mock.json │ ├── no-assets-json │ └── no-assets-json.json │ ├── no-downloads │ └── no-downloads.json │ └── no-main-class │ └── no-main-class.json ├── oxlintrc.json ├── package.json ├── packages ├── asm │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── libs │ │ ├── AnnotationVisitor.ts │ │ ├── AnnotationWriter.ts │ │ ├── Attribute.ts │ │ ├── ByteVector.ts │ │ ├── ClassReader.ts │ │ ├── ClassReaderConstant.ts │ │ ├── ClassVisitor.ts │ │ ├── ClassWriter.ts │ │ ├── ClassWriterConstant.ts │ │ ├── Context.ts │ │ ├── CurrentFrame.ts │ │ ├── Edge.ts │ │ ├── FieldVisitor.ts │ │ ├── FieldWriter.ts │ │ ├── Frame.ts │ │ ├── Handle.ts │ │ ├── Item.ts │ │ ├── Label.ts │ │ ├── MethodVisitor.ts │ │ ├── MethodWriter.ts │ │ ├── MethodWriterConstant.ts │ │ ├── Opcodes.ts │ │ ├── Type.ts │ │ ├── TypePath.ts │ │ ├── TypeReference.ts │ │ ├── bits.ts │ │ └── utils.ts │ ├── package.json │ └── tsconfig.json ├── bytebuffer │ ├── .npmignore │ ├── README.md │ ├── build.ts │ ├── debug.ts │ ├── hex.ts │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ ├── varint32.ts │ └── varint64.ts ├── client │ ├── .npmignore │ ├── README.md │ ├── channel.ts │ ├── coders.test.ts │ ├── coders.ts │ ├── index.ts │ ├── lan.ts │ ├── package.json │ ├── packet.ts │ ├── status.ts │ ├── test.ts │ └── tsconfig.json ├── core │ ├── .npmignore │ ├── README.md │ ├── __mocks__ │ │ └── utils.ts │ ├── diagnose.test.ts │ ├── diagnose.ts │ ├── folder.test.ts │ ├── folder.ts │ ├── index.ts │ ├── launch.e2e.test.ts │ ├── launch.test.ts │ ├── launch.ts │ ├── package.json │ ├── platform.ts │ ├── tsconfig.json │ ├── utils.test.ts │ ├── utils.ts │ ├── version.test.ts │ └── version.ts ├── curseforge │ ├── .npmignore │ ├── README.md │ ├── index.test.ts │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── discord-rpc │ ├── .gitignore │ ├── .npmignore │ ├── Client.ts │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── structures │ │ ├── Base.ts │ │ ├── CertifiedDevice.ts │ │ ├── Channel.ts │ │ ├── ClientUser.ts │ │ ├── Guild.ts │ │ ├── Lobby.ts │ │ ├── Message.ts │ │ ├── Transport.ts │ │ ├── User.ts │ │ └── VoiceSettings.ts │ ├── transport │ │ └── IPC.ts │ ├── tsconfig.json │ └── utils │ │ ├── RPCError.ts │ │ └── TypedEmitter.ts ├── file-transfer │ ├── README.md │ ├── agent.ts │ ├── checkpoint.ts │ ├── download.ts │ ├── error.ts │ ├── index.ts │ ├── metadata.ts │ ├── package.json │ ├── progress.ts │ ├── rangePolicy.ts │ ├── retry.ts │ ├── tsconfig.json │ └── validator.ts ├── forge-site-parser │ ├── .npmignore │ ├── README.md │ ├── index.test.ts │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── game-data │ ├── .npmignore │ ├── README.md │ ├── index.ts │ ├── level.test.ts │ ├── level.ts │ ├── package.json │ ├── serverDat.test.ts │ ├── serverDat.ts │ └── tsconfig.json ├── gamesetting │ ├── .npmignore │ ├── README.md │ ├── index.test.ts │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── installer │ ├── .npmignore │ ├── README.md │ ├── diagnose.ts │ ├── downloadTask.ts │ ├── fabric.ts │ ├── forge.ts │ ├── index.ts │ ├── install.e2e.test.ts │ ├── java-runtime.ts │ ├── java.test.ts │ ├── java.ts │ ├── labymod.ts │ ├── liteloader.ts │ ├── manifest.test.ts │ ├── manifest.ts │ ├── minecraft.ts │ ├── neoForged.ts │ ├── optifine.ts │ ├── package.json │ ├── profile.ts │ ├── quilt.ts │ ├── tsconfig.json │ ├── unzip.ts │ ├── utils.ts │ └── zipValdiator.ts ├── mod-parser │ ├── .npmignore │ ├── README.md │ ├── fabric.test.ts │ ├── fabric.ts │ ├── forge.test.ts │ ├── forge.ts │ ├── forgeConfig.ts │ ├── index.ts │ ├── liteloader.test.ts │ ├── liteloader.ts │ ├── package.json │ ├── quilt.ts │ └── tsconfig.json ├── model │ ├── .npmignore │ ├── README.md │ ├── block.ts │ ├── index.ts │ ├── package.json │ ├── player-model.ts │ ├── player.ts │ └── tsconfig.json ├── modrinth │ ├── README.md │ ├── index.test.ts │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── types.ts ├── nat-api │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── lib │ │ ├── device.ts │ │ ├── pmp.ts │ │ ├── ssdp.ts │ │ └── upnp.ts │ ├── package.json │ └── tsconfig.json ├── nbt │ ├── .npmignore │ ├── README.md │ ├── index.test.ts │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ ├── utils.ts │ └── zlib │ │ ├── index.browser.ts │ │ ├── index.ts │ │ ├── package.json │ │ └── tsconfig.json ├── oxlint-config │ ├── .gitignore │ ├── oxlintrc.json │ └── package.json ├── resourcepack │ ├── .npmignore │ ├── README.md │ ├── format.ts │ ├── index.ts │ ├── modelLoader.ts │ ├── package.json │ ├── resourceManager.test.ts │ ├── resourceManager.ts │ ├── resourcePack.test.ts │ ├── resourcePack.ts │ └── tsconfig.json ├── semver │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── index.ts │ ├── operators.ts │ ├── package.json │ ├── range.ts │ ├── semver.ts │ └── tsconfig.json ├── system │ ├── .npmignore │ ├── README.md │ ├── index.browser.ts │ ├── index.ts │ ├── package.json │ ├── system.test.ts │ ├── system.ts │ └── tsconfig.json ├── task │ ├── .npmignore │ ├── README.md │ ├── index.test.ts │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── text-component │ ├── .npmignore │ ├── README.md │ ├── index.test.ts │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── undici-shim │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── unzip │ ├── .npmignore │ ├── README.md │ ├── index.test.ts │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── user-offline-uuid │ ├── index.browser.ts │ ├── index.ts │ ├── package.json │ └── tsconfig.json └── user │ ├── .npmignore │ ├── README.md │ ├── gameProfile.ts │ ├── index.ts │ ├── microsoft.test.ts │ ├── microsoft.ts │ ├── mojang.test.ts │ ├── mojang.ts │ ├── offline.test.ts │ ├── offline.ts │ ├── package.json │ ├── tsconfig.json │ ├── yggdrasil.test.ts │ └── yggdrasil.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── protocol.json ├── scripts └── docs.ts ├── test-setup.ts ├── test.d.ts ├── tsconfig.json └── vitest.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.md text 4 | *.ts text 5 | *.js text 6 | *.json text 7 | 8 | *.png binary 9 | *.jpg binary 10 | *.zip binary 11 | *.jar binary 12 | *.litemod binary 13 | *.dat binary 14 | *.mcmeta binary 15 | *.mca binary 16 | 17 | # Don't allow people to merge changes to these generated files, because the result 18 | # may be invalid. You need to run "rush update" again. 19 | pnpm-lock.yaml merge=binary 20 | shrinkwrap.yaml merge=binary 21 | npm-shrinkwrap.json merge=binary 22 | yarn.lock merge=binary 23 | 24 | *.json linguist-language=JSON-with-Comments 25 | 26 | -------------------------------------------------------------------------------- /.github/actions/prepare-pr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@actions/core": "1.10.0" 5 | }, 6 | "dependencies": { 7 | "conventional-recommended-bump": "^6.1.0", 8 | "conventional-commits-parser": "^3.2.4", 9 | "@types/conventional-recommended-bump": "^6.1.0", 10 | "@types/conventional-commits-parser": "^3.0.3", 11 | "semver": "^7.5.4", 12 | "@types/semver": "^7.3.13", 13 | "@types/node": "~16" 14 | } 15 | } -------------------------------------------------------------------------------- /.github/actions/prepare-pr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node" 5 | } 6 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'packages/*/*.ts' 9 | - 'packages/*/package.json' 10 | - 'pnpm-lock.yaml' 11 | - '.github/**' 12 | 13 | jobs: 14 | automation: 15 | runs-on: ubuntu-latest 16 | 17 | if: ${{ !startsWith(github.event.head_commit.message, 'chore(release)') }} 18 | 19 | strategy: 20 | matrix: 21 | node-version: [20] 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | - uses: pnpm/action-setup@v2.0.1 28 | with: 29 | version: 9.15.3 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | cache: 'pnpm' 35 | - uses: actions/setup-java@v1 36 | with: 37 | java-version: '12.X' 38 | architecture: x64 39 | - name: Install 40 | run: | 41 | pnpm install 42 | env: 43 | CI: true 44 | - name: Lint 45 | run: | 46 | pnpm lint 47 | - name: Test 48 | run: | 49 | pnpm coverage 50 | - name: Build 51 | run: | 52 | pnpm --parallel build:type 53 | pnpm --parallel build:cjs 54 | pnpm --parallel build:esm 55 | pnpm --parallel build:browser 56 | - name: Prepare Pull Request 57 | id: vars 58 | run: | 59 | pnpm tsx .github/actions/prepare-pr/index.ts 60 | env: 61 | CI: true 62 | - name: Update lock file 63 | run: | 64 | pnpm install --frozen-lockfile=false 65 | - name: Create Pull Request 66 | uses: peter-evans/create-pull-request@v3 67 | with: 68 | assignees: ci010 69 | reviewers: ci010 70 | branch: prepare-release 71 | title: ${{ steps.vars.outputs.title }} 72 | body: ${{ steps.vars.outputs.body }} 73 | commit-message: ${{ steps.vars.outputs.message }} 74 | token: ${{ secrets.GITHUB_TOKEN }} 75 | - name: Upload Coverage 76 | uses: actions/upload-artifact@master 77 | with: 78 | name: coverage 79 | path: coverage 80 | 81 | 82 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Github Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'docs/**' 9 | - README.md 10 | - CONTRIBUTE.md 11 | - 'packages/*/README.md' 12 | workflow_dispatch: 13 | 14 | jobs: 15 | page-deploy: 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [20] 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: pnpm/action-setup@v2.0.1 25 | with: 26 | version: 9.15.3 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | cache: 'pnpm' 32 | - name: Install 33 | run: | 34 | pnpm install 35 | env: 36 | CI: true 37 | - name: Build Docs 38 | run: | 39 | pnpm build:docs 40 | - name: Deploy to Github Pages 41 | uses: crazy-max/ghaction-github-pages@v1 42 | with: 43 | target_branch: gh-pages 44 | build_dir: docs/ 45 | env: 46 | GITHUB_PAT: ${{ secrets.GITHUB_PAT }} 47 | - name: Notify the xmcl-page 48 | uses: benc-uk/workflow-dispatch@v1 49 | with: 50 | token: ${{ secrets.GITHUB_PAT }} 51 | repo: Voxelum/xmcl-page 52 | workflow: deploy.yaml 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | if: ${{ startsWith(github.event.head_commit.message, 'chore(release)') }} 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | ref: ${{ github.sha }} 18 | - uses: pnpm/action-setup@v2.0.1 19 | with: 20 | version: 9.15.3 21 | - name: Use Node.js 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: 20 25 | registry-url: 'https://registry.npmjs.org' 26 | - name: Install 27 | run: | 28 | pnpm install 29 | env: 30 | CI: true 31 | - name: Build 32 | run: | 33 | pnpm --parallel build:type 34 | pnpm --parallel build:cjs 35 | pnpm --parallel build:esm 36 | pnpm --parallel build:browser 37 | - name: Publish 38 | run: | 39 | pnpm -r publish --access public --no-git-checks 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 43 | - name: Publish Tag 44 | run: | 45 | node --eval "console.log(JSON.parse(require('fs').readFileSync('package.json').toString()).version)" > tag.log 46 | git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/voxelum/minecraft-launcher-core-node.git" 47 | git config user.name ci010 48 | git config user.email cijhn@hotmail.com 49 | git tag -f $(cat tag.log) 50 | git push origin tag $(cat tag.log) -f 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | - name: Build Docs 54 | run: | 55 | pnpm build:docs 56 | - name: Deploy to Github Pages 57 | uses: crazy-max/ghaction-github-pages@v1 58 | with: 59 | target_branch: gh-pages 60 | build_dir: docs/ 61 | env: 62 | GITHUB_PAT: ${{ secrets.GITHUB_PAT }} 63 | -------------------------------------------------------------------------------- /.github/workflows/validation.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [20] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: pnpm/action-setup@v2.0.1 19 | with: 20 | version: 9.15.3 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: 'pnpm' 26 | - uses: actions/setup-java@v1 27 | with: 28 | java-version: '12.X' 29 | architecture: x64 30 | - name: Install 31 | run: | 32 | pnpm i 33 | env: 34 | CI: true 35 | - name: Lint and Compile 36 | run: | 37 | pnpm lint 38 | pnpm --parallel build:type 39 | - name: Test 40 | run: | 41 | pnpm coverage 42 | env: 43 | CI: true 44 | - name: Upload Coverage 45 | uses: actions/upload-artifact@master 46 | with: 47 | name: coverage 48 | path: coverage 49 | 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.cache 3 | 4 | /mock/libraries 5 | /mock/assets 6 | /mock/logs 7 | 8 | docs/**/*.md 9 | /docs/sidebar.json 10 | 11 | **/.vitepress/cache/** 12 | 13 | *.tsbuildinfo 14 | 15 | # Logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Runtime data 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | 69 | # next.js build output 70 | .next 71 | 72 | packages/**/*.js 73 | packages/**/*.d.ts 74 | packages/**/*.js.map 75 | packages/**/package-lock.json 76 | 77 | /temp 78 | 79 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.vscode 3 | /assets 4 | /src/tests 5 | /dest/tests 6 | *.log 7 | /temp 8 | *.tsbuildinfo 9 | tslint.json 10 | **/.vitepress/cache/** -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.tabSize": 2, 4 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "typescript", 6 | "tsconfig": "tsconfig.json", 7 | "option": "watch", 8 | "problemMatcher": [ 9 | "$tsc-watch" 10 | ] 11 | }, 12 | { 13 | "type": "typescript", 14 | "tsconfig": "tsconfig.json", 15 | "problemMatcher": [ 16 | "$tsc" 17 | ] 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "test", 22 | "problemMatcher": [] 23 | }, 24 | { 25 | "type": "npm", 26 | "script": "build", 27 | "problemMatcher": [] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | Feel free to contribute to the project. You can fork the project and make PR to here just like any other github project. 4 | 5 | - [Contribute](#contribute) 6 | - [Getting Started](#getting-started) 7 | - [Pre-requirements](#pre-requirements) 8 | - [Setup Dev Workspace](#setup-dev-workspace) 9 | - [How to Dev](#how-to-dev) 10 | - [How to Build](#how-to-build) 11 | - [Commit You Code](#commit-you-code) 12 | - [Before Submit PR](#before-submit-pr) 13 | 14 | ## Getting Started 15 | 16 | Introduce how to setup environment, modify code and submit PR. 17 | 18 | ### Pre-requirements 19 | 20 | - Node.js >=18.17.0 21 | - pnpm (can be setup by node [corepack](https://nodejs.org/api/corepack.html)) 22 | 23 | ### Setup Dev Workspace 24 | 25 | Use git to clone the project 26 | 27 | ```sh 28 | git clone https://github.com/Voxelum/minecraft-launcher-core-node 29 | ``` 30 | 31 | If you don't have `pnpm`, you can use corepack to setup 32 | 33 | ```sh 34 | corepack enable 35 | ``` 36 | 37 | Now you should have `pnpm` installed. You can run the install command to install dependencies 38 | 39 | ```sh 40 | pnpm install 41 | ``` 42 | 43 | ### How to Dev 44 | 45 | The code are split into separated projects, which layed in under `packages` folder. Most of them are really simple (only one `index.ts` file or few files). 46 | 47 | For pure library developer, we recommend you to have TDD (Test Driven Development) style to develop. You can add some test first, and then add the code to pass the test. 48 | 49 | We are using the [vitest](https://vitest.dev/) as the test framework. You can run it by `pnpm run test` or `pnpm vitest` to run the test in watch mode. 50 | 51 | Please see vitest document for more detailed operations. 52 | 53 | ### How to Build 54 | 55 | You can run build the project locally to make sure it pass the PR validation. The build is separated in 3 parts: 56 | 57 | 1. Build esm mjs files 58 | 2. Build cjs js files 59 | 3. Build typescript dts files 60 | 61 | You need to leverage the pnpm recursive option to run the build command. 62 | 63 | ```sh 64 | pnpm build:cjs --parallel # build cjs 65 | pnpm build:esm --parallel # build mjs 66 | pnpm build:type --parallel # build dts 67 | ``` 68 | 69 | ### Commit You Code 70 | 71 | Make sure you commit your code using the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0-beta.2/). The github action will use your commit message to generate changelog and bump version. They are important. 72 | 73 | ### Before Submit PR 74 | 75 | Make sure you run `pnpm lint` to pass lint so the PR validation can pass. 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CI010 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mock/modpack.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/modpack.zip -------------------------------------------------------------------------------- /mock/mods/dummy-mod.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/dummy-mod.jar -------------------------------------------------------------------------------- /mock/mods/fabric-sample-2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/fabric-sample-2.jar -------------------------------------------------------------------------------- /mock/mods/fabric-sample.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/fabric-sample.jar -------------------------------------------------------------------------------- /mock/mods/sample-ccc-mod.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/sample-ccc-mod.jar -------------------------------------------------------------------------------- /mock/mods/sample-mod-1.13-no-deps.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/sample-mod-1.13-no-deps.jar -------------------------------------------------------------------------------- /mock/mods/sample-mod-1.13.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/sample-mod-1.13.jar -------------------------------------------------------------------------------- /mock/mods/sample-mod.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/sample-mod.jar -------------------------------------------------------------------------------- /mock/mods/sample-mod.litemod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/sample-mod.litemod -------------------------------------------------------------------------------- /mock/mods/sample-nei-mod.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/sample-nei-mod.jar -------------------------------------------------------------------------------- /mock/mods/tweak-class.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/mods/tweak-class.jar -------------------------------------------------------------------------------- /mock/options.txt: -------------------------------------------------------------------------------- 1 | version:512 2 | invertYMouse:false 3 | mouseSensitivity:0.47887325 4 | fov:0.0 5 | gamma:1.0 6 | saturation:0.0 7 | renderDistance:12 8 | guiScale:0 9 | particles:1 10 | bobView:true 11 | anaglyph3d:false 12 | maxFps:120 13 | fboEnable:true 14 | difficulty:1 15 | fancyGraphics:false 16 | ao:1 17 | renderClouds:false 18 | resourcePacks:["Xray Ultimate 1.12 v2.2.1.zip"] 19 | incompatibleResourcePacks:[] 20 | lastServer:play.mcndsj.com 21 | lang:en_US 22 | chatVisibility:0 23 | chatColors:true 24 | chatLinks:true 25 | chatLinksPrompt:true 26 | chatOpacity:1.0 27 | snooperEnabled:true 28 | fullscreen:false 29 | enableVsync:true 30 | useVbo:true 31 | hideServerAddress:false 32 | advancedItemTooltips:false 33 | pauseOnLostFocus:true 34 | touchscreen:false 35 | overrideWidth:0 36 | overrideHeight:0 37 | heldItemTooltips:true 38 | chatHeightFocused:1.0 39 | chatHeightUnfocused:0.44366196 40 | chatScale:1.0 41 | chatWidth:1.0 42 | showInventoryAchievementHint:false 43 | mipmapLevels:4 44 | forceUnicodeFont:false 45 | reducedDebugInfo:false 46 | useNativeTransport:true 47 | entityShadows:true 48 | mainHand:right 49 | attackIndicator:1 50 | showSubtitles:false 51 | realmsNotifications:true 52 | enableWeakAttacks:false 53 | autoJump:true 54 | key_key.attack:-100 55 | key_key.use:-99 56 | key_key.forward:17 57 | key_key.left:30 58 | key_key.back:31 59 | key_key.right:32 60 | key_key.jump:57 61 | key_key.sneak:42 62 | key_key.sprint:29 63 | key_key.drop:16 64 | key_key.inventory:18 65 | key_key.chat:28 66 | key_key.playerlist:15 67 | key_key.pickItem:-98 68 | key_key.command:53 69 | key_key.screenshot:60 70 | key_key.togglePerspective:63 71 | key_key.smoothCamera:0 72 | key_key.fullscreen:87 73 | key_key.spectatorOutlines:0 74 | key_key.swapHands:33 75 | key_key.hotbar.1:2 76 | key_key.hotbar.2:3 77 | key_key.hotbar.3:4 78 | key_key.hotbar.4:5 79 | key_key.hotbar.5:6 80 | key_key.hotbar.6:7 81 | key_key.hotbar.7:8 82 | key_key.hotbar.8:9 83 | key_key.hotbar.9:10 84 | soundCategory_master:1.0 85 | soundCategory_music:1.0 86 | soundCategory_record:1.0 87 | soundCategory_weather:1.0 88 | soundCategory_block:1.0 89 | soundCategory_hostile:1.0 90 | soundCategory_neutral:1.0 91 | soundCategory_player:1.0 92 | soundCategory_ambient:1.0 93 | soundCategory_voice:1.0 94 | modelPart_cape:true 95 | modelPart_jacket:true 96 | modelPart_left_sleeve:true 97 | modelPart_right_sleeve:true 98 | modelPart_left_pants_leg:true 99 | modelPart_right_pants_leg:true 100 | modelPart_hat:true 101 | -------------------------------------------------------------------------------- /mock/pack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/pack.png -------------------------------------------------------------------------------- /mock/resourcepacks/1.14.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/resourcepacks/1.14.4.zip -------------------------------------------------------------------------------- /mock/resourcepacks/empty-resourcepack.zip: -------------------------------------------------------------------------------- 1 | PK -------------------------------------------------------------------------------- /mock/resourcepacks/sample-resourcepack.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/resourcepacks/sample-resourcepack.zip -------------------------------------------------------------------------------- /mock/resourcepacks/sample-resourcepack/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "pack_format": 1, 4 | "description": "Vattic\u0027s Faithful 32x32 pack" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mock/resourcepacks/sample-resourcepack/pack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/resourcepacks/sample-resourcepack/pack.png -------------------------------------------------------------------------------- /mock/saves/1.12.2/region/r.0.0.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/1.12.2/region/r.0.0.mca -------------------------------------------------------------------------------- /mock/saves/1.14.4/region/r.0.0.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/1.14.4/region/r.0.0.mca -------------------------------------------------------------------------------- /mock/saves/1.16.4/region/r.0.0.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/1.16.4/region/r.0.0.mca -------------------------------------------------------------------------------- /mock/saves/1.16.5/region/r.0.0.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/1.16.5/region/r.0.0.mca -------------------------------------------------------------------------------- /mock/saves/1.19.3/region/r.0.0.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/1.19.3/region/r.0.0.mca -------------------------------------------------------------------------------- /mock/saves/sample-map.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/sample-map.zip -------------------------------------------------------------------------------- /mock/saves/sample-map/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/sample-map/icon.png -------------------------------------------------------------------------------- /mock/saves/sample-map/level.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/sample-map/level.dat -------------------------------------------------------------------------------- /mock/saves/sample-map/playerdata/abf81fe9-9f0d-4948-a909-7721a8198ac4.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/sample-map/playerdata/abf81fe9-9f0d-4948-a909-7721a8198ac4.dat -------------------------------------------------------------------------------- /mock/saves/sample-map/region/r.0.0.mca: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/saves/sample-map/region/r.0.0.mca -------------------------------------------------------------------------------- /mock/servers.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voxelum/minecraft-launcher-core-node/461a5d92f45865742d53c9b6230dccc02ad8b274/mock/servers.dat -------------------------------------------------------------------------------- /mock/versions/mock/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "mock", 3 | "assetIndex": {}, 4 | "mainClass": "Main", 5 | "downloads": { 6 | "mock": { 7 | "sha1": "", 8 | "size": 0, 9 | "url": "" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /mock/versions/no-assets-json/no-assets-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "no-assets-json", 3 | "mainClass": "Main", 4 | "downloads": { 5 | "mock": { 6 | "sha1": "", 7 | "size": 0, 8 | "url": "" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /mock/versions/no-downloads/no-downloads.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "no-downloads", 3 | "assetIndex": {}, 4 | "mainClass": "Main" 5 | } -------------------------------------------------------------------------------- /mock/versions/no-main-class/no-main-class.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "no-main-class", 3 | "assetIndex": {}, 4 | "downloads": { 5 | "mock": { 6 | "sha1": "", 7 | "size": 0, 8 | "url": "" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /oxlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/oxlint/configuration_schema.json", 3 | "plugins": [ 4 | "import", 5 | "typescript", 6 | "node", 7 | "unicorn" 8 | ], 9 | "env": { 10 | "browser": true, 11 | "node": true 12 | }, 13 | "ignorePatterns": [ 14 | "**/node_modules/**", 15 | "**/dist/**" 16 | ], 17 | "globals": { 18 | "foo": "readonly" 19 | }, 20 | "settings": {}, 21 | "rules": { 22 | "eqeqeq": "warn", 23 | "import/no-cycle": "error", 24 | "@typescript-eslint/no-unsafe-declaration-merging": "off", 25 | "@typescript-eslint/member-delimiter-style": [ 26 | "error", 27 | { 28 | "multiline": { 29 | "delimiter": "none" 30 | } 31 | } 32 | ], 33 | "@typescript-eslint/ban-types": 0, 34 | "@typescript-eslint/ban-ts-comment": 0, 35 | "@typescript-eslint/explicit-function-return-type": 0, 36 | "@typescript-eslint/explicit-module-boundary-types": 0, 37 | "@typescript-eslint/no-empty-function": 0, 38 | "@typescript-eslint/no-empty-interface": 0, 39 | "@typescript-eslint/no-explicit-any": 0, 40 | "@typescript-eslint/no-non-null-assertion": 0, 41 | "@typescript-eslint/no-unused-expressions": 2, 42 | "@typescript-eslint/no-unused-vars": 0, 43 | "unicorn/no-new-array": 0, 44 | "import/no-absolute-path": 0, 45 | "space-before-function-paren": 0, 46 | "no-useless-constructor": 0, 47 | "no-unused-vars": 0, 48 | "no-use-before-define": 0, 49 | "node/no-callback-literal": 0 50 | }, 51 | "overrides": [ 52 | { 53 | "files": [ 54 | "*.test.ts", 55 | "*.spec.ts" 56 | ], 57 | "rules": { 58 | "@typescript-eslint/no-explicit-any": "off" 59 | } 60 | }, 61 | { 62 | "files": [ 63 | "**/asm/**" 64 | ], 65 | "rules": { 66 | "@typescript-eslint/no-duplicate-enum-values": "off", 67 | "no-dupe-else-if": "off", 68 | "eqeqeq": "off", 69 | "no-this-alias": "off" 70 | } 71 | } 72 | ] 73 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "7.9.0", 4 | "scripts": { 5 | "build:docs": "tsx scripts/docs.ts", 6 | "lint": "oxlint -c ./oxlintrc.json packages/**", 7 | "lint:fix": "oxlint -c ./oxlintrc.json packages/** --fix", 8 | "test": "vitest", 9 | "coverage": "vitest run --coverage" 10 | }, 11 | "packageManager": "pnpm@9.15.3", 12 | "devDependencies": { 13 | "tsx": "^4.19.2", 14 | "typedoc": "^0.27.6", 15 | "oxlint": "^0.15.5", 16 | "typescript": "^5.3.3", 17 | "@vitest/coverage-v8": "^2.1.8", 18 | "vitest": "^2.1.8" 19 | } 20 | } -------------------------------------------------------------------------------- /packages/asm/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/asm/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2000-2011 INRIA, France Telecom 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holders nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 29 | THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /packages/asm/README.md: -------------------------------------------------------------------------------- 1 | # ASM Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/asm.svg)](https://www.npmjs.com/package/@xmcl/asm) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/asm.svg)](https://npmjs.com/@xmcl/asm) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/asm)](https://packagephobia.now.sh/result?p=@xmcl/asm) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Parse Java bytecode, which port from [java asm package](https://asm.ow2.io/). 10 | 11 | ## Usage 12 | 13 | ### Visit java class in jar file 14 | 15 | The usage is just like asm library in java: 16 | 17 | ```ts 18 | import { AnnotationVisitor, ClassReader, ClassVisitor, MethodVisitor, Opcodes } from '@xmcl/asm' 19 | 20 | 21 | class CustomClassVisitor extends ClassVisitor { 22 | public constructor() { 23 | super(Opcodes.ASM5); 24 | } 25 | 26 | // visit the class 27 | visit(version: number, access: number, name: string, signature: string, superName: string, interfaces: string[]): void { 28 | } 29 | 30 | // visit method 31 | public visitMethod(access: number, name: string, desc: string, signature: string, exceptions: string[]) { 32 | return null; 33 | } 34 | 35 | // visit field 36 | public visitField(access: number, name: string, desc: string, signature: string, value: any) { 37 | return null; 38 | } 39 | } 40 | 41 | const visitor = new CustomClassVisitor(); 42 | const classData: Buffer = await fs.readFile("path/to/some.class"); 43 | new ClassReader(classData).accept(visitor); 44 | ``` 45 | -------------------------------------------------------------------------------- /packages/asm/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ASM: a very small and fast Java bytecode manipulation framework 3 | * Copyright (c) 2000-2011 INRIA, France Telecom 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions 8 | * are met: 9 | * 1. Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * 2. Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 3. Neither the name of the copyright holders nor the names of its 15 | * contributors may be used to endorse or promote products derived from 16 | * this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 22 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 28 | * THE POSSIBILITY OF SUCH DAMAGE. 29 | * 30 | * @module @xmcl/asm 31 | * 32 | */ 33 | 34 | export * from './libs/AnnotationVisitor' 35 | export * from './libs/Attribute' 36 | export * from './libs/ClassReader' 37 | export * from './libs/ClassVisitor' 38 | // import { ClassWriter } from './src/ClassWriter' 39 | export * from './libs/FieldVisitor' 40 | // import { FieldWriter } from './src/FieldWriter' 41 | export * from './libs/Handle' 42 | export * from './libs/Label' 43 | export * from './libs/MethodVisitor' 44 | export * from './libs/Opcodes' 45 | export * from './libs/Type' 46 | export * from './libs/TypePath' 47 | export * from './libs/TypeReference' 48 | -------------------------------------------------------------------------------- /packages/asm/libs/ClassReaderConstant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * True to enable stack map frames support. 3 | */ 4 | export const FRAMES = true 5 | 6 | /** 7 | * True to enable signatures support. 8 | */ 9 | export const SIGNATURES = true 10 | 11 | /** 12 | * True to enable annotations support. 13 | */ 14 | export const ANNOTATIONS = true -------------------------------------------------------------------------------- /packages/asm/libs/MethodWriterConstant.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pseudo access flag used to denote constructors. 3 | */ 4 | export const ACC_CONSTRUCTOR = 524288 -------------------------------------------------------------------------------- /packages/asm/libs/bits.ts: -------------------------------------------------------------------------------- 1 | const int8 = new Int8Array(4) 2 | const int32 = new Int32Array(int8.buffer, 0, 1) 3 | const float32 = new Float32Array(int8.buffer, 0, 1) 4 | 5 | export const SHORT_MIN = -32768 6 | export const SHORT_MAX = 32768 7 | 8 | export function intBitsToFloat(bits: number): number { 9 | int32[0] = bits 10 | return float32[0] 11 | } 12 | 13 | export function floatToIntBits(bits: number): number { 14 | float32[0] = bits 15 | return int32[0] 16 | } 17 | 18 | const int16 = new Int16Array(4) 19 | const int64 = new Int32Array(int16.buffer, 0, 2) 20 | const float64 = new Float64Array(int16.buffer, 0, 1) 21 | 22 | export function longBitsToDouble (bits: bigint): number { 23 | int64[0] = Number(bits >> 32n & 0xffffffffn) 24 | int64[1] = Number(bits & 0xffffffffn) 25 | return float64[0] 26 | } 27 | 28 | export function doubleToLongBits (double: number): bigint { 29 | float64[0] = double 30 | return BigInt(int64[1]) << 32n & BigInt(int64[0]) 31 | } 32 | -------------------------------------------------------------------------------- /packages/asm/libs/utils.ts: -------------------------------------------------------------------------------- 1 | export function assert(v: T | null): asserts v is T { 2 | if (!v) { throw new Error('nullptr') } 3 | } 4 | 5 | export function notnull(v: T | null): T { 6 | if (!v) { throw new Error('nullptr') } 7 | return v 8 | } 9 | -------------------------------------------------------------------------------- /packages/asm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/asm", 3 | "version": "1.0.1", 4 | "main": "./index.ts", 5 | "description": "Java ASM module transferred into the typescript", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "publishConfig": { 10 | "access": "public", 11 | "main": "./dist/index.js", 12 | "module": "./dist/index.mjs" 13 | }, 14 | "scripts": { 15 | "build:type": "tsc", 16 | "build:cjs": "esbuild --target=es2020 --platform=neutral --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 17 | "build:esm": "esbuild --target=es2020 --platform=neutral --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 22 | }, 23 | "author": "cijhn@hotmail.com", 24 | "keywords": [ 25 | "typescript", 26 | "nodejs", 27 | "asm" 28 | ], 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 32 | }, 33 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 34 | "devDependencies": { 35 | "@types/node": "~18.15.11", 36 | "@xmcl/oxlint-config": "workspace:^*", 37 | "esbuild": "^0.17.16", 38 | "oxlint": "^0.15.5", 39 | "tslib": "^2.5.0", 40 | "typescript": "^5.3.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/asm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "libs/utils.ts", 4 | "libs/AnnotationVisitor.ts", 5 | "libs/bits.ts", 6 | "libs/ClassReader.ts", 7 | "libs/ClassWriter.ts", 8 | "libs/CurrentFrame.ts", 9 | "libs/FieldVisitor.ts", 10 | "libs/Frame.ts", 11 | "libs/Item.ts", 12 | "libs/MethodVisitor.ts", 13 | "libs/Opcodes.ts", 14 | "libs/TypePath.ts", 15 | "libs/AnnotationWriter.ts", 16 | "libs/Attribute.ts", 17 | "libs/ByteVector.ts", 18 | "libs/ClassVisitor.ts", 19 | "libs/Context.ts", 20 | "libs/Edge.ts", 21 | "libs/FieldWriter.ts", 22 | "libs/Handle.ts", 23 | "libs/Label.ts", 24 | "libs/MethodWriter.ts", 25 | "libs/Type.ts", 26 | "libs/TypeReference.ts", 27 | "libs/ClassReaderConstant.ts", 28 | "libs/MethodWriterConstant.ts", 29 | "libs/ClassWriterConstant.ts", 30 | "index.ts" 31 | ], 32 | "extends": "../../tsconfig.json", 33 | "compilerOptions": { 34 | "outDir": "./dist" 35 | }, 36 | } -------------------------------------------------------------------------------- /packages/bytebuffer/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/bytebuffer/README.md: -------------------------------------------------------------------------------- 1 | # ByteBuffer Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/bytebuffer.svg)](https://www.npmjs.com/package/@xmcl/bytebuffer) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/bytebuffer.svg)](https://npmjs.com/@xmcl/bytebuffer) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/bytebuffer)](https://packagephobia.now.sh/result?p=@xmcl/bytebuffer) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Provide some functions to query Minecraft server status. Port from [bytebuffer.js](https://github.com/protobufjs/bytebuffer.js). 10 | 11 | ## Usage 12 | 13 | Should similar to the bytebuffer in java. 14 | 15 | You can also reference the [bytebuffer.js](https://github.com/protobufjs/bytebuffer.js). 16 | 17 | There are sevearl notable differences: 18 | 19 | - Remove string methods as nodejs have better string encoding/decoding support. 20 | - This module only release one version using `DataView`. Should compatible for both browser and nodejs. 21 | - This module use `BigInt` to represent the `long` type. 22 | - Split non-common methods support into separate files. (Hope to reduce the build size) 23 | - Support esm (mjs) 24 | 25 | Common usage: 26 | 27 | ```ts 28 | import { ByteBuffer } from '@xmcl/bytebuffer' 29 | const bb = ByteBuffer.allocate(10) 30 | // similar to java's ByteBuffer 31 | ``` 32 | 33 | Using extra methods: 34 | 35 | ```ts 36 | import { ByteBuffer } from '@xmcl/bytebuffer' 37 | import '@xmcl/bytebuffer/varint64' // importing this will inject the varint64 methods to ByteBuffer 38 | 39 | const bb = ByteBuffer.allocate(10) 40 | bb.writeVarint64(BigInt(1234567890)) // now this is avaiable! 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/bytebuffer/build.ts: -------------------------------------------------------------------------------- 1 | import { build as esbuild } from 'esbuild' 2 | 3 | async function build() { 4 | await esbuild({ 5 | entryPoints: ['index.ts', 'debug.ts', 'hex.ts', 'varint32.ts', 'varint64.ts'], 6 | platform: 'neutral', 7 | sourcemap: true, 8 | format: 'cjs', 9 | target: 'node16', 10 | outdir: 'dist', 11 | }) 12 | await esbuild({ 13 | entryPoints: ['index.ts', 'debug.ts', 'hex.ts', 'varint32.ts', 'varint64.ts'], 14 | platform: 'neutral', 15 | sourcemap: true, 16 | format: 'esm', 17 | outExtension: { '.js': '.mjs' }, 18 | target: 'node16', 19 | outdir: 'dist', 20 | }) 21 | } 22 | 23 | build() 24 | -------------------------------------------------------------------------------- /packages/bytebuffer/hex.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from '.' 2 | // encodings/hex 3 | declare module './index' { 4 | interface ByteBuffer { 5 | toHex(begin: number | undefined, end: number | undefined): string 6 | } 7 | } 8 | 9 | /** 10 | * Encodes this ByteBuffer's contents to a hex encoded string. 11 | * @param {number=} begin Offset to begin at. Defaults to {@link ByteBuffer#offset}. 12 | * @param {number=} end Offset to end at. Defaults to {@link ByteBuffer#limit}. 13 | * @returns {string} Hex encoded string 14 | * @expose 15 | */ 16 | ByteBuffer.prototype.toHex = function (begin?: number, end?: number): string { 17 | begin = typeof begin === 'undefined' ? this.offset : begin 18 | end = typeof end === 'undefined' ? this.limit : end 19 | if (!this.noAssert) { 20 | if (typeof begin !== 'number' || begin % 1 !== 0) { throw TypeError('Illegal begin: Not an integer') } 21 | begin >>>= 0 22 | if (typeof end !== 'number' || end % 1 !== 0) { throw TypeError('Illegal end: Not an integer') } 23 | end >>>= 0 24 | if (begin < 0 || begin > end || end > this.buffer.byteLength) { throw RangeError('Illegal range: 0 <= ' + begin + ' <= ' + end + ' <= ' + this.buffer.byteLength) } 25 | } 26 | const out = new Array(end - begin) 27 | let b 28 | while (begin < end) { 29 | b = this.view.getUint8(begin++) 30 | if (b < 0x10) { out.push('0', b.toString(16)) } else { out.push(b.toString(16)) } 31 | } 32 | return out.join('') 33 | } 34 | 35 | /** 36 | * Decodes a hex encoded string to a ByteBuffer. 37 | * @param {string} str String to decode 38 | * @param {boolean=} littleEndian Whether to use little or big endian byte order. Defaults to 39 | * {@link ByteBuffer.DEFAULT_ENDIAN}. 40 | * @param {boolean=} noAssert Whether to skip assertions of offsets and values. Defaults to 41 | * {@link ByteBuffer.DEFAULT_NOASSERT}. 42 | * @returns {!ByteBuffer} ByteBuffer 43 | * @expose 44 | */ 45 | export const fromHex = function (str: string, littleEndian: boolean | undefined, noAssert: boolean | undefined): ByteBuffer { 46 | if (!noAssert) { 47 | if (typeof str !== 'string') { throw TypeError('Illegal str: Not a string') } 48 | if (str.length % 2 !== 0) { throw TypeError('Illegal str: Length not a multiple of 2') } 49 | } 50 | const k = str.length 51 | const bb = new ByteBuffer((k / 2) | 0, littleEndian) 52 | let b 53 | let j = 0 54 | for (let i = 0; i < k; i += 2) { 55 | b = parseInt(str.substring(i, i + 2), 16) 56 | if (!noAssert) { 57 | if (!isFinite(b) || b < 0 || b > 255) { throw TypeError('Illegal str: Contains non-hex characters') } 58 | } 59 | bb.view.setUint8(j++, b) 60 | } 61 | bb.limit = j 62 | return bb 63 | } 64 | -------------------------------------------------------------------------------- /packages/bytebuffer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/bytebuffer", 3 | "version": "0.1.1", 4 | "main": "./index.ts", 5 | "description": "A bytebuffer module ported from bytebuffer.js", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "publishConfig": { 10 | "access": "public", 11 | "main": "./dist/index.js", 12 | "module": "./dist/index.mjs", 13 | "exports": { 14 | ".": { 15 | "import": "./dist/index.mjs", 16 | "require": "./dist/index.js" 17 | }, 18 | "debug": { 19 | "import": "./dist/debug.mjs", 20 | "require": "./dist/debug.js" 21 | }, 22 | "hex": { 23 | "import": "./dist/hex.mjs", 24 | "require": "./dist/hex.js" 25 | }, 26 | "varint32": { 27 | "import": "./dist/varint32.mjs", 28 | "require": "./dist/varint32.js" 29 | }, 30 | "varint64": { 31 | "import": "./dist/varint64.mjs", 32 | "require": "./dist/varint64.js" 33 | } 34 | } 35 | }, 36 | "scripts": { 37 | "build:type": "tsc", 38 | "build:esm": "tsx build.ts" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 43 | }, 44 | "author": "cijhn@hotmail.com", 45 | "keywords": [ 46 | "typescript", 47 | "nodejs", 48 | "electron", 49 | "bytebuffer" 50 | ], 51 | "license": "MIT", 52 | "bugs": { 53 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 54 | }, 55 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 56 | "devDependencies": { 57 | "@types/node": "~18.15.11", 58 | "@xmcl/oxlint-config": "workspace:^*", 59 | "bytebuffer": "^5.0.1", 60 | "esbuild": "^0.17.16", 61 | "oxlint": "^0.15.5", 62 | "tslib": "^2.5.0", 63 | "typescript": "^5.3.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/bytebuffer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "*.ts", 4 | ], 5 | "exclude": ["node_modules"], 6 | "extends": "../../tsconfig.json", 7 | "compilerOptions": { 8 | "outDir": "./dist" 9 | }, 10 | } -------------------------------------------------------------------------------- /packages/client/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # Client Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/client.svg)](https://www.npmjs.com/package/@xmcl/client) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/client.svg)](https://npmjs.com/@xmcl/client) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/client)](https://packagephobia.now.sh/result?p=@xmcl/client) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Minecraft socket pipeline utilities. Support Minecraft lan server discovery. 10 | 11 | ## Usage 12 | 13 | ### Ping Minecraft Server 14 | 15 | Read sever info (server ip, port) and fetch its status (ping, server motd): 16 | 17 | ```ts 18 | import { queryStatus, Status, QueryOptions } from '@xmcl/client' 19 | const serverInfo = { 20 | host: 'your host', 21 | port: 25565, // be default 22 | }; 23 | const options: QueryOptions = { 24 | /** 25 | * see http://wiki.vg/Protocol_version_numbers 26 | */ 27 | protocol: 203, 28 | }; 29 | const rawStatusJson: Status = await fetchStatus(info, options); 30 | ``` 31 | 32 | ### Detect LAN Minecraft Server 33 | 34 | You can detect if player share LAN server. 35 | 36 | Or you can fake a LAN server. 37 | 38 | ```ts 39 | import { MinecraftLanDiscover, LanServerInfo } from '@xmcl/client' 40 | const discover = new MinecraftLanDiscover(); 41 | 42 | await discover.bind(); // start to listen any lan server 43 | 44 | discover.on('discover', ({ motd, port }: LanServerInfo) => { 45 | console.log(motd); // server motd 46 | console.log(port); // server port 47 | }) 48 | 49 | const isReady = discover.isReady // a boolean represent whether the discover is ready to use 50 | 51 | // you can also fake a lan server 52 | discover.broadcast({ 53 | motd: 'your motd', 54 | port: 2384 // fake port 55 | }); 56 | // fake LAN server is useful when you want to implement the P2P connection between two players 57 | 58 | dicover.destroy(); // stop listening 59 | 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /packages/client/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The client for Minecraft protocol. I can create the connection with Minecraft server and ping the server status. 3 | * 4 | * You can use {@link queryStatus} with {@link QueryOptions} to ping a {@link Status} of a server 5 | * 6 | * @packageDocumentation 7 | * @module @xmcl/client 8 | */ 9 | 10 | export * from './coders' 11 | export * from './packet' 12 | export * from './channel' 13 | export * from './status' 14 | export * from './lan' 15 | -------------------------------------------------------------------------------- /packages/client/lan.ts: -------------------------------------------------------------------------------- 1 | import { Socket, createSocket, RemoteInfo } from 'dgram' 2 | import { EventEmitter } from 'events' 3 | 4 | export const LAN_MULTICAST_ADDR = '224.0.2.60' 5 | export const LAN_MULTICAST_ADDR_V6 = 'FF75:230::60' 6 | export const LAN_MULTICAST_PORT = 4445 7 | 8 | export interface MinecraftLanDiscover { 9 | on(channel: 'discover', listener: (event: LanServerInfo & { remote: RemoteInfo }) => void): this 10 | once(channel: 'discover', listener: (event: LanServerInfo & { remote: RemoteInfo }) => void): this 11 | addListener(channel: 'discover', listener: (event: LanServerInfo & { remote: RemoteInfo }) => void): this 12 | removeListener(channel: 'discover', listener: (event: LanServerInfo & { remote: RemoteInfo }) => void): this 13 | } 14 | 15 | export class MinecraftLanDiscover extends EventEmitter { 16 | readonly socket: Socket 17 | 18 | #ready = false 19 | #group: string 20 | 21 | get isReady() { 22 | return this.#ready 23 | } 24 | 25 | constructor(type: 'udp4' | 'udp6' = 'udp4') { 26 | super() 27 | const sock = createSocket({ type, reuseAddr: true }) 28 | 29 | this.#group = type === 'udp4' ? LAN_MULTICAST_ADDR : LAN_MULTICAST_ADDR_V6 30 | 31 | sock.on('listening', () => { 32 | const address = sock.address() 33 | sock.addMembership(this.#group, type === 'udp4' ? address.address : undefined) 34 | sock.setMulticastTTL(128) 35 | sock.setBroadcast(true) 36 | this.#ready = true 37 | }) 38 | 39 | sock.on('message', (buf, remote) => { 40 | const content = buf.toString('utf-8') 41 | 42 | const motdRegx = /\[MOTD\](.+)\[\/MOTD\]/g 43 | const portRegx = /\[AD\](.+)\[\/AD\]/g 44 | 45 | const portResult = portRegx.exec(content) 46 | if (!portResult || !portResult[1]) { 47 | // emit error 48 | } else { 49 | const motd = motdRegx.exec(content)?.[1] ?? '' 50 | const port = Number.parseInt(portResult[1]) 51 | this.emit('discover', { motd, port, remote }) 52 | } 53 | }) 54 | 55 | this.socket = sock 56 | } 57 | 58 | broadcast(inf: LanServerInfo) { 59 | return new Promise((resolve, reject) => { 60 | this.socket.send(`[MOTD]${inf.motd}[/MOTD][AD]${inf.port}[/AD]`, LAN_MULTICAST_PORT, this.#group, (err, bytes) => { 61 | if (err) reject(err) 62 | else resolve(bytes) 63 | }) 64 | }) 65 | } 66 | 67 | bind(): Promise { 68 | return new Promise((resolve, reject) => { 69 | this.socket.bind(LAN_MULTICAST_PORT, () => { 70 | resolve() 71 | }).once('error', (e) => { 72 | reject(e) 73 | }) 74 | }) 75 | } 76 | 77 | destroy(): Promise { 78 | return new Promise((resolve) => { 79 | this.socket.close(resolve) 80 | }) 81 | } 82 | } 83 | 84 | export interface LanServerInfo { 85 | motd: string 86 | port: number 87 | } 88 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/client", 3 | "version": "3.2.0", 4 | "main": "./index.ts", 5 | "description": "Minecraft socket pipeline utilities. Support Minecraft lan server discovery.", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "publishConfig": { 10 | "access": "public", 11 | "main": "./dist/index.js", 12 | "module": "./dist/index.mjs" 13 | }, 14 | "scripts": { 15 | "build:type": "tsc", 16 | "build:cjs": "esbuild --target=node16 --platform=node --external:long --external:@xmcl/* --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 17 | "build:esm": "esbuild --target=node16 --platform=node --external:long --external:@xmcl/* --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 18 | }, 19 | "dependencies": { 20 | "@xmcl/bytebuffer": "workspace:^*", 21 | "@xmcl/nbt": "workspace:^*", 22 | "@xmcl/text-component": "workspace:^*" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 27 | }, 28 | "author": "cijhn@hotmail.com", 29 | "keywords": [ 30 | "minecraft", 31 | "typescript", 32 | "minecraft-launcher", 33 | "nodejs", 34 | "electron" 35 | ], 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 39 | }, 40 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 41 | "devDependencies": { 42 | "@types/node": "~18.15.11", 43 | "@xmcl/oxlint-config": "workspace:^*", 44 | "esbuild": "^0.17.16", 45 | "oxlint": "^0.15.5", 46 | "tslib": "^2.5.0", 47 | "typescript": "^5.3.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/client/test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { queryStatus, Status } from './index' 3 | import { describe, test, expect } from 'vitest' 4 | 5 | describe('Server', () => { 6 | const root = path.normalize(path.join(__dirname, '..', '..', 'mock')) 7 | describe('Ping', () => { 8 | // testContext.slow(3000); 9 | // test("should fetch server frame", async () => { 10 | // const frame = await queryStatus({ host: "mc.hypixel.net" }); 11 | // expect(frame); 12 | // expect(frame.ping !== -1).toBeTruthy(); 13 | // }); 14 | // test("should control the port", async () => { 15 | // await expect(queryStatus({ host: "mc.hypixel.net", port: 138 }, { timeout: 500, retryTimes: 0 })) 16 | // .rejects 17 | // .toBeTruthy(); 18 | // }); 19 | test('should capture timeout exception', async () => { 20 | await expect(queryStatus({ 21 | host: 'crafterr.me', 22 | }, { timeout: 100 })) 23 | .rejects 24 | .toBeTruthy() 25 | }) 26 | // test("should fetch server info and ping", async () => { 27 | // const status = await queryStatus({ 28 | // host: "mc.hypixel.net", 29 | // }, { timeout: 10000 }); 30 | // expect(typeof status === "object").toBeTruthy(); 31 | // expect((typeof status.description === "object") || (typeof status.description === "string")).toBeTruthy(); 32 | // expect(status.ping).not.toEqual(-1); 33 | // }); 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "index.ts", 4 | "channel.ts", 5 | "coders.ts", 6 | "packet.ts", 7 | "lan.ts", 8 | "status.ts" 9 | ], 10 | "extends": "../../tsconfig.json", 11 | "compilerOptions": { 12 | "outDir": "./dist" 13 | }, 14 | } -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Launcher Core Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/core.svg)](https://www.npmjs.com/package/@xmcl/core) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/core.svg)](https://npmjs.com/@xmcl/core) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/core)](https://packagephobia.now.sh/result?p=@xmcl/core) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Provide the core function to parse Minecraft version and launch. 10 | 11 | ## Usage 12 | 13 | ### Parse Version JSON 14 | 15 | Parse minecraft version as a resolved version, which is used for launching process. You can also read version info from it if you want. 16 | 17 | ```ts 18 | import { Version } from "@xmcl/core"; 19 | const minecraftLocation: string; 20 | const minecraftVersionId: string; 21 | 22 | const resolvedVersion: ResolvedVersion = await Version.parse(minecraftLocation, minecraftVersionId); 23 | ``` 24 | 25 | ### Diagnose 26 | 27 | Get the report of the version. It can check if version missing assets/libraries. 28 | 29 | ```ts 30 | import { MinecraftLocation, diagnose, ResolvedVersion } from "@xmcl/core"; 31 | 32 | const minecraft: MinecraftLocation; 33 | const version: string; // version string like 1.13 34 | const resolvedVersion: ResolvedVersion = await Version.parse(minecraft, version); 35 | 36 | const report: MinecraftIssueReport = await diagnose(resolvedVersion.id, resolvedVersion.minecraftDirectory); 37 | 38 | const issues: MinecraftIssues[] = report.issues; 39 | 40 | for (let issue of issues) { 41 | switch (issue.role) { 42 | case "minecraftJar": // your jar has problem 43 | case "versionJson": // your json has problem 44 | case "library": // your lib might be missing or corrupted 45 | case "assets": // some assets are missing or corrupted 46 | // and so on 47 | } 48 | } 49 | ``` 50 | 51 | 52 | ### Launch Game 53 | 54 | Launch minecraft from a version: 55 | 56 | ```ts 57 | import { launch } from "@xmcl/core" 58 | const version: string; // full version id, like 1.13, or your forge version like, 1.13-forge- 59 | const javaPath: string; // java executable path 60 | const gamePath: string; // .minecraft path 61 | const proc: Promise = launch({ gamePath, javaPath, version }); 62 | ``` 63 | 64 | Detach from the parent process. So your launcher's exit/crash won't affact the Minecraft running. 65 | 66 | ```ts 67 | const proc: Promise = Launcher.launch({ gamePath, javaPath, version, extraExecOption: { detached: true } }); 68 | ``` 69 | -------------------------------------------------------------------------------- /packages/core/__mocks__/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | let existedFiles = [] as string[] 4 | let checksums: Record = {} 5 | 6 | /** @ignore */ 7 | export function exists(name: string) { 8 | return Promise.resolve(existedFiles.indexOf(name) !== -1) 9 | } 10 | /** @ignore */ 11 | export function checksum(name: string, algorithm: string) { 12 | return Promise.resolve(checksums[name]) 13 | } 14 | /** @ignore */ 15 | export function __addChecksum(record: object) { 16 | for (const [k, v] of Object.entries(record)) { 17 | checksums[path.join(/* root, */ k)] = v 18 | } 19 | } 20 | /** @ignore */ 21 | export function __addExistedFile(name: string) { 22 | existedFiles.push(path.join(/* root, */ name)) 23 | } 24 | /** @ignore */ 25 | export function __reset() { 26 | checksums = {} 27 | existedFiles = [] 28 | } 29 | /** @ignore */ 30 | export function isNotNull(v: any) { return !!v } 31 | -------------------------------------------------------------------------------- /packages/core/folder.test.ts: -------------------------------------------------------------------------------- 1 | import { sep } from 'path' 2 | import { MinecraftFolder } from '.' 3 | import { describe, test, expect } from 'vitest' 4 | 5 | describe('MinecraftFolder', () => { 6 | const mc = new MinecraftFolder('root') 7 | test('It should get the basic path according to root', () => { 8 | expect(mc.root).toBe('root') 9 | expect(mc.getAsset('aabbbb')).toBe('root/assets/objects/aa/aabbbb'.replace(/\//g, sep)) 10 | expect(mc.getAssetsIndex('1.2')).toBe('root/assets/indexes/1.2.json'.replace(/\//g, sep)) 11 | expect(mc.getMod('test.jar')).toBe('root/mods/test.jar'.replace(/\//g, sep)) 12 | expect(mc.getResourcePack('test.zip')).toBe('root/resourcepacks/test.zip'.replace(/\//g, sep)) 13 | expect(mc.getVersionJar('1.12')).toBe('root/versions/1.12/1.12.jar'.replace(/\//g, sep)) 14 | expect(mc.getVersionJson('1.12')).toBe('root/versions/1.12/1.12.json'.replace(/\//g, sep)) 15 | expect(mc.getVersionRoot('1.12')).toBe('root/versions/1.12'.replace(/\//g, sep)) 16 | expect(mc.getLog('somelog.txt')).toBe('root/logs/somelog.txt'.replace(/\//g, sep)) 17 | expect(mc.getNativesRoot('1.12')).toBe('root/versions/1.12/1.12-natives'.replace(/\//g, sep)) 18 | expect(mc.getMapIcon('somemap')).toBe('root/saves/somemap/icon.png'.replace(/\//g, sep)) 19 | expect(mc.getMapInfo('somemap')).toBe('root/saves/somemap/level.dat'.replace(/\//g, sep)) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/core/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The core package for launching Minecraft. 3 | * It provides the {@link Version.parse} function to parse Minecraft version, 4 | * and the {@link launch} function to launch the game. 5 | * 6 | * @packageDocumentation 7 | * @module @xmcl/core 8 | */ 9 | 10 | export * from './launch' 11 | export * from './version' 12 | export * from './platform' 13 | export * from './folder' 14 | export * from './diagnose' 15 | export { 16 | checksum, 17 | } from './utils' 18 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/core", 3 | "version": "2.14.1", 4 | "main": "./index.ts", 5 | "description": "Minecraft version.json parsing and game launching.", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=node16 --platform=node --external:@xmcl/* --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=node16 --platform=node --external:@xmcl/* --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "access": "public", 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs" 18 | }, 19 | "dependencies": { 20 | "@xmcl/unzip": "workspace:^*" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 25 | }, 26 | "author": "cijhn@hotmail.com", 27 | "keywords": [ 28 | "minecraft", 29 | "typescript", 30 | "minecraft-launcher", 31 | "nodejs", 32 | "electron" 33 | ], 34 | "sideEffects": false, 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 38 | }, 39 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 40 | "devDependencies": { 41 | "@types/node": "~18.15.11", 42 | "@xmcl/oxlint-config": "workspace:^*", 43 | "esbuild": "^0.17.16", 44 | "oxlint": "^0.15.5", 45 | "typescript": "^5.3.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/platform.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os' 2 | 3 | /** 4 | * The platform information related to current operating system. 5 | */ 6 | export interface Platform { 7 | /** 8 | * The system name of the platform. This name is majorly used for download. 9 | */ 10 | name: 'osx' | 'linux' | 'windows' | 'unknown' 11 | /** 12 | * The version of the os. It should be the value of `os.release()`. 13 | */ 14 | version: string 15 | /** 16 | * The direct output of `os.arch()`. Should look like x86 or x64. 17 | */ 18 | arch: 'x86' | 'x64' | string 19 | } 20 | 21 | /** 22 | * Get Minecraft style platform info. (Majorly used to enable/disable native dependencies) 23 | */ 24 | export function getPlatform(): Platform { 25 | const arch = os.arch() 26 | const version = os.release() 27 | switch (os.platform()) { 28 | case 'darwin': 29 | return { name: 'osx', version, arch } 30 | case 'linux': 31 | return { name: 'linux', version, arch } 32 | case 'win32': 33 | return { name: 'windows', version, arch } 34 | default: 35 | return { name: 'unknown', version, arch } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts", 4 | "launch.ts", 5 | "version.ts", 6 | "folder.ts", 7 | "platform.ts", 8 | "utils.ts", 9 | "diagnose.ts" 10 | ], 11 | "extends": "../../tsconfig.json", 12 | "compilerOptions": { 13 | "outDir": "./dist" 14 | }, 15 | } -------------------------------------------------------------------------------- /packages/core/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { join, normalize } from 'path' 2 | import { checksum, validateSha1 } from './utils' 3 | import { describe, test, expect } from 'vitest' 4 | 5 | const root = normalize(join(__dirname, '..', '..', 'mock')) 6 | 7 | describe('util', () => { 8 | describe('#validateSha1', () => { 9 | test('should return false if the file not found', async () => { 10 | await expect(validateSha1(join(root, 'test.ss'))) 11 | .resolves 12 | .toEqual(false) 13 | }) 14 | test('should return false if the sha not matched', async () => { 15 | await expect(validateSha1(join(root, 'options.txt'), 'abc')) 16 | .resolves 17 | .toEqual(false) 18 | }) 19 | test('should return true if the sha matched', async () => { 20 | await expect(validateSha1(join(root, 'options.txt'), 'e1719c99026ae3714ea24f13f50cdf6894844511')) 21 | .resolves 22 | .toEqual(true) 23 | }) 24 | test('should return true if the sha not given and file existed in non strict', async () => { 25 | await expect(validateSha1(join(root, 'options.txt'))) 26 | .resolves 27 | .toEqual(true) 28 | }) 29 | test('should return false if the sha not given and file existed in strict', async () => { 30 | await expect(validateSha1(join(root, 'options.txt'), undefined, true)) 31 | .resolves 32 | .toEqual(false) 33 | }) 34 | }) 35 | describe('#checksum', () => { 36 | test('should get sha1', async () => { 37 | await expect(checksum(join(root, 'options.txt'), 'sha1')) 38 | .resolves 39 | .toEqual('e1719c99026ae3714ea24f13f50cdf6894844511') 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /packages/core/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | */ 4 | 5 | import { createHash } from 'crypto' 6 | import { constants, createReadStream } from 'fs' 7 | import { access } from 'fs/promises' 8 | import { pipeline } from 'stream/promises' 9 | 10 | /** @internal */ 11 | export function exists(file: string) { 12 | return access(file, constants.F_OK).then(() => true, () => false) 13 | } 14 | /** 15 | * Validate the sha1 value of the file 16 | * @internal 17 | */ 18 | export async function validateSha1(target: string, hash?: string, strict = false) { 19 | if (await access(target).then(() => false, () => true)) { return false } 20 | if (!hash) { return !strict } 21 | const sha1 = await checksum(target, 'sha1') 22 | return sha1 === hash 23 | } 24 | /** 25 | * Return the sha1 of a file 26 | * @internal 27 | */ 28 | export async function checksum(target: string, algorithm: string) { 29 | const hash = createHash(algorithm).setEncoding('hex') 30 | try { 31 | await pipeline(createReadStream(target), hash) 32 | } catch (e) { 33 | if ((e as any).code === 'ENOENT') { 34 | return undefined 35 | } 36 | } 37 | return hash.read() 38 | } 39 | /** 40 | * @internal 41 | */ 42 | export function isNotNull(v: T | undefined): v is T { 43 | return v !== undefined 44 | } 45 | -------------------------------------------------------------------------------- /packages/curseforge/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/curseforge/README.md: -------------------------------------------------------------------------------- 1 | # Curseforge API 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/curseforge.svg)](https://www.npmjs.com/package/@xmcl/curseforge) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/curseforge.svg)](https://npmjs.com/@xmcl/curseforge) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/curseforge)](https://packagephobia.now.sh/result?p=@xmcl/curseforge) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Provide the curseforge API wrapper in https://docs.curseforge.com/ 10 | 11 | ## Usage 12 | 13 | This package is depending on undici for HTTP in nodejs, and the browser version will use browser `fetch` instead of undici. 14 | 15 | ### Find Curseforge Mods by search keyword 16 | 17 | You can search curseforge mods by keyword 18 | 19 | ```ts 20 | import { CurseforgeV1Client, SearchOptions } from '@xmcl/curseforge' 21 | 22 | const api = new CurseforgeV1Client(process.env.CURSEFORGE_API_KEY) 23 | const searchOptions: SearchOptions = { 24 | categoryId: 6, // 6 is mod, 25 | searchFilter: 'search-keyword', 26 | }; 27 | const result = await api.searchMods(searchOptions) 28 | const pageSize = result.pagination.pageSize 29 | const total = result.pagination.total 30 | const totalPages = Math.ceil(total / pageSize) 31 | const mods: Mod[] = result.data // mod details 32 | ``` 33 | 34 | ### The Mod detail 35 | 36 | You can get the mod detail by mod id 37 | 38 | ```ts 39 | import { CurseforgeV1Client } from '@xmcl/curseforge' 40 | const api = new CurseforgeV1Client(process.env.CURSEFORGE_API_KEY) 41 | const mod = await api.getMod(123) 42 | console.log(mod.id) // 123 43 | console.log(mod.name) // The mod name 44 | console.log(mod.authors) // [{ id: 123, name: 'The author name' }] 45 | console.log(mod.summary) // The mod summary 46 | ``` 47 | 48 | ### Get Mod File List 49 | 50 | ```ts 51 | // sample code for get file list for a mod 52 | const api = new CurseforgeV1Client(process.env.CURSEFORGE_API_KEY) 53 | const { data: files, pagination } = await api.getModFiles({ 54 | modId: 1, // your mod id 55 | gameVersion: '1.16.5', // support minecraft 1.16.5, optional 56 | modLoaderType: FileModLoaderType.Fabric, // only fabric, optional 57 | }) 58 | const pageSize = pagination.pageSize 59 | const index = pagination.index 60 | const total = pagination.total // total count 61 | const pages = Math.ceil(total / pageSize) // total pages 62 | const firstFile = files[0] 63 | ``` 64 | -------------------------------------------------------------------------------- /packages/curseforge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/curseforge", 3 | "version": "2.2.0", 4 | "main": "./index.ts", 5 | "module": "./index.ts", 6 | "sideEffects": false, 7 | "description": "An implementation of curseforge (official) API in https://docs.curseforge.com/", 8 | "scripts": { 9 | "build:type": "tsc", 10 | "build:cjs": "esbuild --target=node16 --external:undici --platform=node --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 11 | "build:esm": "esbuild --target=node16 --external:undici --platform=node --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts", 12 | "build:browser": "esbuild --target=es2020 --sourcemap --alias:undici=undici-shim --format=esm --bundle --outfile=dist/index.browser.mjs index.ts" 13 | }, 14 | "engines": { 15 | "node": ">=16.4" 16 | }, 17 | "publishConfig": { 18 | "access": "public", 19 | "main": "./dist/index.js", 20 | "module": "./dist/index.mjs", 21 | "browser": "./dist/index.browser.mjs" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 26 | }, 27 | "author": "cijhn@hotmail.com", 28 | "keywords": [ 29 | "minecraft", 30 | "typescript", 31 | "minecraft-launcher", 32 | "nodejs", 33 | "electron", 34 | "curseforge" 35 | ], 36 | "license": "MIT", 37 | "dependencies": { 38 | "undici": "7.2.3" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 42 | }, 43 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 44 | "devDependencies": { 45 | "esbuild": "^0.17.16", 46 | "undici-shim": "workspace:^*" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/curseforge/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts" 4 | ], 5 | "extends": "../../tsconfig.json", 6 | "compilerOptions": { 7 | "outDir": "./dist" 8 | }, 9 | } -------------------------------------------------------------------------------- /packages/discord-rpc/.npmignore: -------------------------------------------------------------------------------- 1 | # Folder 2 | examples/ 3 | __test__/ 4 | .vscode/ 5 | .github/ 6 | .husky/ 7 | .yarn/ 8 | docs/ 9 | test/ 10 | src/ 11 | 12 | # File 13 | tsconfig.json 14 | .yarnrc.yml 15 | .prettierrc 16 | *.tgz 17 | 18 | # lockfile 19 | package-lock.json 20 | yarn.lock -------------------------------------------------------------------------------- /packages/discord-rpc/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2023 xhayper 4 | Copyright 2022 devsnek 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 9 | -------------------------------------------------------------------------------- /packages/discord-rpc/README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

@xhayper/discord-rpc

5 |
6 |

7 | npm version 8 | discord 9 | license 10 |

11 |
12 | 13 | 14 | ## About 15 | 16 | `@xhayper/discord-rpc` is a fork of [discordjs/RPC](https://github.com/discordjs/RPC) with type safety and some additional features. 17 | 18 | Looking for Deno version? Check out [discord_rpc_deno](https://github.com/xhayper/discord-rpc-deno)! 19 | 20 | ## Features 21 | 22 | - flatpak / snap support 23 | - Low dependencies count 24 | - Proper error exception 25 | - Up-To-Date with Discord's IPC command / event list 26 | - Less than 100kb bundle size 27 | 28 | ## Optional packages (when using WebSocket) 29 | 30 | - [bufferutil](https://www.npmjs.com/package/bufferutil) 31 | - [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) 32 | 33 | ## Example 34 | 35 | ```ts 36 | import { Client } from "@xhayper/discord-rpc"; 37 | 38 | const client = new Client({ 39 | clientId: "123456789012345678" 40 | }); 41 | 42 | client.on("ready", () => { 43 | client.user?.setActivity({ 44 | state: "Hello, world!" 45 | }); 46 | }); 47 | 48 | client.login(); 49 | ``` 50 | 51 | ## Compatibility 52 | 53 | | OS | Normal | snap | flatpak | 54 | | ------- | ------ | ---- | ------- | 55 | | Windows | Y | - | - | 56 | | macOS | Y | - | - | 57 | | Linux | Y | Y | Y | 58 | 59 | - Linux is tested on Kubuntu 22.04 60 | 61 | ## Credits 62 | 63 | - [discordjs](https://github.com/discordjs): Making [discordjs/RPC](https://github.com/discordjs/RPC) 64 | - [JakeMakesStuff](https://github.com/JakeMakesStuff): [snap support](https://github.com/discordjs/RPC/pull/152) 65 | - [Snazzah](https://github.com/Snazzah): [snap + flatpak support](https://github.com/Snazzah/SublimeDiscordRP/blob/c13e60cdbc5de8147881bb232f2339722c2b46b4/discord_ipc/__init__.py#L208) 66 | - [leonardssh](https://github.com/leonardssh): Making [coc-discord-rpc](https://github.com/leonardssh/coc-discord-rpc) which inspried me to make this package due to how old [discordjs/RPC](https://github.com/discordjs/RPC) is 67 | -------------------------------------------------------------------------------- /packages/discord-rpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Client' 2 | 3 | export * from './structures/ClientUser' 4 | export * from './structures/CertifiedDevice' 5 | export * from './structures/Channel' 6 | export * from './structures/Guild' 7 | export * from './structures/Lobby' 8 | export * from './structures/User' 9 | export * from './structures/VoiceSettings' 10 | export * from './structures/Transport' 11 | export * from './structures/Message' 12 | 13 | export { type FormatFunction } from './transport/IPC' 14 | -------------------------------------------------------------------------------- /packages/discord-rpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/discord-rpc", 3 | "version": "1.1.2", 4 | "description": "a fork of discordjs/RPC", 5 | "main": "./index.ts", 6 | "author": "xhayper", 7 | "license": "ISC", 8 | "scripts": { 9 | "build:type": "tsc", 10 | "build:cjs": "esbuild --target=node16 --external:undici --platform=node --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 11 | "build:esm": "esbuild --target=node16 --external:undici --platform=node --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 12 | }, 13 | "publishConfig": { 14 | "access": "public", 15 | "main": "./dist/index.js", 16 | "module": "./dist/index.mjs" 17 | }, 18 | "keywords": [ 19 | "typescript", 20 | "discord", 21 | "ipc", 22 | "rpc", 23 | "rich-presence", 24 | "discord-rpc", 25 | "discord-ipc" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 33 | }, 34 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 35 | "sideEffects": false, 36 | "dependencies": { 37 | "discord-api-types": "^0.37.39", 38 | "undici": "7.2.3" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "~18", 42 | "oxlint": "^0.15.5", 43 | "@xmcl/oxlint-config": "workspace:^*", 44 | "esbuild": "^0.17.16", 45 | "typescript": "^5.3.3", 46 | "tsx": "^3.12.1" 47 | }, 48 | "engines": { 49 | "node": ">=20" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/discord-rpc/structures/Base.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from '../Client' 2 | 3 | export class Base { 4 | /** 5 | * the client instance 6 | */ 7 | client: Client 8 | 9 | constructor(client: Client) { 10 | this.client = client 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/discord-rpc/structures/CertifiedDevice.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from '../Client' 2 | import { Base } from './Base' 3 | 4 | export enum DeviceType { 5 | AUDIO_INPUT = 'audioinput', 6 | AUDIO_OUTPUT = 'audiooutput', 7 | VIDEO_INPUT = 'videoinput', 8 | } 9 | 10 | export interface Vendor { 11 | /** 12 | * name of the vendor 13 | */ 14 | name: string 15 | /** 16 | * url for the vendor 17 | */ 18 | url: string 19 | } 20 | 21 | export interface Model { 22 | /** 23 | * name of the model 24 | */ 25 | name: string 26 | /** 27 | * url for the model 28 | */ 29 | url: string 30 | } 31 | 32 | export class CertifiedDevice extends Base { 33 | /** 34 | * the type of device 35 | */ 36 | type: DeviceType 37 | /** 38 | * the device's Windows UUID 39 | */ 40 | id: string 41 | /** 42 | * the hardware vendor 43 | */ 44 | vendor: Vendor 45 | /** 46 | * the model of the product 47 | */ 48 | model: Model 49 | /** 50 | * UUIDs of related devices 51 | */ 52 | related: string[] 53 | /** 54 | * if the device's native echo cancellation is enabled 55 | */ 56 | echo_cancellation?: boolean 57 | /** 58 | * if the device's native noise suppression is enabled 59 | */ 60 | noise_suppression?: boolean 61 | /** 62 | * if the device's native automatic gain control is enabled 63 | */ 64 | automatic_gain_control?: boolean 65 | /** 66 | * if the device is hardware muted 67 | */ 68 | hardware_mute?: boolean 69 | 70 | constructor(client: Client, props: Record) { 71 | super(client) 72 | Object.assign(this, props) 73 | 74 | this.type = props.type 75 | this.id = props.id 76 | this.vendor = props.vendor 77 | this.model = props.model 78 | this.related = props.related 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/discord-rpc/structures/Channel.ts: -------------------------------------------------------------------------------- 1 | import type { ChannelType, GatewayVoiceState } from 'discord-api-types/v10' 2 | import type { Client } from '../Client' 3 | import { Message } from './Message' 4 | import { Base } from './Base' 5 | 6 | export class Channel extends Base { 7 | /** 8 | * channel id 9 | */ 10 | id: string 11 | /** 12 | * channel's guild id 13 | */ 14 | guild_id?: string 15 | /** 16 | * channel name 17 | */ 18 | name: string 19 | /** 20 | * channel type (guild text: 0, guild voice: 2, dm: 1, group dm: 3) 21 | */ 22 | type: ChannelType 23 | /** 24 | * (text) channel topic 25 | */ 26 | topic?: string 27 | /** 28 | * (voice) bitrate of voice channel 29 | */ 30 | bitrate?: number 31 | /** 32 | * (voice) user limit of voice channel (0 for none) 33 | */ 34 | user_limit?: number 35 | /** 36 | * position of channel in channel list 37 | */ 38 | position?: number 39 | /** 40 | * (voice) channel's voice states 41 | */ 42 | voice_states?: GatewayVoiceState[] 43 | /** 44 | * (text) channel's messages 45 | */ 46 | messages?: Message[] 47 | 48 | constructor(client: Client, props: Record) { 49 | super(client) 50 | Object.assign(this, props) 51 | 52 | this.id = props.id 53 | this.guild_id = props.guild_id 54 | this.name = props.name 55 | this.type = props.type 56 | this.topic = props.topic 57 | this.bitrate = props.bitrate 58 | this.user_limit = props.user_limit 59 | this.position = props.position 60 | this.voice_states = props.voice_states 61 | this.messages = props.messages?.map((messgeData: any) => new Message(client, messgeData)) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/discord-rpc/structures/Guild.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from '../Client' 2 | import { type User } from './User' 3 | import { Base } from './Base' 4 | 5 | export class Guild extends Base { 6 | /** 7 | * guild id 8 | */ 9 | id: string 10 | /** 11 | * guild name (2-100 characters, excluding trailing and leading whitespace) 12 | */ 13 | name: string 14 | icon_url: string | null 15 | /** 16 | * guild member list 17 | * (always an empty array) 18 | * @deprecated 19 | */ 20 | members: User[] = [] // Always an empty array 21 | /** 22 | * the vanity url code for the guild 23 | */ 24 | vanity_url_code: string | null 25 | 26 | constructor(client: Client, props: Record) { 27 | super(client) 28 | Object.assign(this, props) 29 | 30 | this.id = props.id 31 | this.name = props.name 32 | this.icon_url = props.icon_url 33 | this.vanity_url_code = props.vanity_url_code 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/discord-rpc/structures/Lobby.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import type { GatewayVoiceState } from 'discord-api-types/v10' 3 | import type { Client } from '../Client' 4 | import { type User } from './User' 5 | import { Base } from './Base' 6 | 7 | export enum LobbyType { 8 | PRIVATE = 1, 9 | PUBLIC = 2, 10 | } 11 | 12 | export class Lobby extends Base { 13 | application_id: string 14 | capacity: number 15 | id: string 16 | locked: boolean 17 | members: { metadata: any; user: User }[] 18 | metadata: any 19 | owner_id: string 20 | region: string 21 | secret: string 22 | type: LobbyType 23 | voice_states: GatewayVoiceState 24 | 25 | constructor(client: Client, props: Record) { 26 | super(client) 27 | Object.assign(this, props) 28 | 29 | this.application_id = props.application_id 30 | this.capacity = props.capacity 31 | this.id = props.id 32 | this.locked = props.locked 33 | this.members = props.members 34 | this.metadata = props.metadata 35 | this.owner_id = props.owner_id 36 | this.region = props.region 37 | this.secret = props.secret 38 | this.type = props.type 39 | this.voice_states = props.voice_states 40 | } 41 | 42 | async joinVoice(): Promise { 43 | await this.client.request('CONNECT_TO_LOBBY_VOICE', { id: this.id }) 44 | } 45 | 46 | async leaveVoice(): Promise { 47 | await this.client.request('DISCONNECT_FROM_LOBBY_VOICE', { id: this.id }) 48 | } 49 | 50 | async update( 51 | type?: LobbyType, 52 | owner_id?: string, 53 | capacity?: number, 54 | locked?: boolean, 55 | metadata?: any, 56 | ): Promise { 57 | this.type = type ?? this.type 58 | this.owner_id = owner_id ?? this.owner_id 59 | this.capacity = capacity ?? this.capacity 60 | this.locked = locked ?? this.locked 61 | this.metadata = metadata ?? this.metadata 62 | 63 | await this.client.request('UPDATE_LOBBY', { 64 | id: this.id, 65 | type, 66 | owner_id, 67 | capacity, 68 | locked, 69 | metadata, 70 | }) 71 | } 72 | 73 | async updateMember(userId: string, metadata?: any): Promise { 74 | await this.client.request('UPDATE_LOBBY_MEMBER', { lobby_id: this.id, user_id: userId, metadata }) 75 | } 76 | 77 | async disconnect(): Promise { 78 | await this.client.request('DISCONNECT_FROM_LOBBY', { id: this.id }) 79 | } 80 | 81 | async delete(): Promise { 82 | await this.client.request('DELETE_LOBBY', { id: this.id }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/discord-rpc/structures/Message.ts: -------------------------------------------------------------------------------- 1 | import type { APIEmbed, APIAttachment, MessageType } from 'discord-api-types/v10' 2 | import type { Client } from '../Client' 3 | import { Base } from './Base' 4 | import { User } from './User' 5 | 6 | export class Message extends Base { 7 | /** 8 | * id of the message 9 | */ 10 | id: string 11 | /** 12 | * if the message's author is blocked 13 | */ 14 | blocked: boolean 15 | /** 16 | * if the message is sent by a bot 17 | */ 18 | bot: boolean 19 | /** 20 | * contents of the message 21 | */ 22 | content: string 23 | content_parsed: any[] 24 | /** 25 | * author's server nickname 26 | */ 27 | nick: string 28 | author_color: string 29 | /** 30 | * when this message was edited (or null if never) 31 | */ 32 | edited_timestamp: string | null 33 | /** 34 | * when this message was sent 35 | */ 36 | timestamp: string 37 | /** 38 | * whether this was a TTS message 39 | */ 40 | tts: boolean 41 | /** 42 | * users specifically mentioned in the message 43 | */ 44 | mentions: User[] 45 | /** 46 | * whether this message mentions everyone 47 | */ 48 | mention_everyone: boolean 49 | /** 50 | * roles specifically mentioned in this message 51 | */ 52 | mention_roles: string[] 53 | /** 54 | * any embedded content 55 | */ 56 | embeds: APIEmbed[] 57 | /** 58 | * any attached files 59 | */ 60 | attachments: APIAttachment[] 61 | /** 62 | * the author of this message 63 | */ 64 | author: User 65 | /** 66 | * whether this message is pinned 67 | */ 68 | pinned: boolean 69 | /** 70 | * [type of message](https://discord.com/developers/docs/resources/channel#message-object-message-types) 71 | */ 72 | type: MessageType 73 | 74 | constructor(client: Client, props: Record) { 75 | super(client) 76 | Object.assign(this, props) 77 | 78 | this.id = props.id 79 | this.blocked = props.blocked 80 | this.bot = props.bot 81 | this.content = props.content 82 | this.content_parsed = props.content_parsed 83 | this.nick = props.nick 84 | this.author_color = props.author_color 85 | this.edited_timestamp = props.edited_timestamp 86 | this.timestamp = props.timestamp 87 | this.tts = props.tts 88 | this.mentions = props.mentions.map((mentionData: any) => new User(client, mentionData)) 89 | this.mention_everyone = props.mention_everyone 90 | this.mention_roles = props.mention_roles 91 | this.embeds = props.embeds 92 | this.attachments = props.attachments 93 | this.author = new User(client, props.author) 94 | this.pinned = props.pinned 95 | this.type = props.type 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/discord-rpc/structures/User.ts: -------------------------------------------------------------------------------- 1 | import type { UserFlags, UserPremiumType, PresenceUpdateStatus, GatewayActivity } from 'discord-api-types/v10' 2 | import type { Client } from '../Client' 3 | import { Base } from './Base' 4 | 5 | export class User extends Base { 6 | /** 7 | * the user's id 8 | */ 9 | id: string 10 | /** 11 | * the user's username, not unique across the platform 12 | */ 13 | username: string 14 | /** 15 | * the user's 4-digit discord-tag 16 | */ 17 | discriminator: string 18 | /** 19 | * the user's [avatar hash](https://discord.com/developers/docs/reference#image-formatting) 20 | */ 21 | avatar: string | null 22 | /** 23 | * the [flags](https://discord.com/developers/docs/resources/user#user-object-user-flags) on a user's account 24 | */ 25 | flags?: UserFlags | undefined 26 | /** 27 | * the [type of Nitro subscription](https://discord.com/developers/docs/resources/user#user-object-premium-types) on a user's account 28 | */ 29 | premium_type?: UserPremiumType | undefined 30 | /** 31 | * the public [flags](https://discord.com/developers/docs/resources/user#user-object-user-flags) on a user's account 32 | */ 33 | public_flags?: UserFlags | undefined 34 | 35 | /** 36 | * user's rich presence 37 | */ 38 | presence?: 39 | | { 40 | status?: PresenceUpdateStatus 41 | activities?: GatewayActivity[] 42 | } 43 | | undefined 44 | 45 | avatar_decoration?: string | null 46 | 47 | constructor(client: Client, props: Record) { 48 | super(client) 49 | Object.assign(this, props) 50 | 51 | // word can't explains how much i hate this 52 | this.id = props.id 53 | this.username = props.username 54 | this.discriminator = props.discriminator 55 | this.avatar = props.avatar 56 | } 57 | 58 | /** 59 | * The URL to the user's avatar. 60 | */ 61 | get avatarUrl() { 62 | const isAnimated = this.avatar && this.avatar.startsWith('a_') 63 | return this.avatar 64 | ? `${this.client.cdnHost}/avatars/${this.id}/${this.avatar}${isAnimated ? '.gif' : '.png'}` 65 | : this.defaultAvatarUrl 66 | } 67 | 68 | /** 69 | * The URL to the user's default avatar. (avatar that is used when user have no avatar) 70 | */ 71 | get defaultAvatarUrl() { 72 | return `${this.client.cdnHost}/embed/avatars/${parseInt(this.discriminator.substring(1)) % 5}.png` 73 | } 74 | 75 | /** 76 | * User's tag 77 | */ 78 | get tag() { 79 | return `${this.username}#${this.discriminator}` 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/discord-rpc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist" 4 | }, 5 | "extends": "../../tsconfig.json", 6 | "include": ["index.ts", "Client.ts", "utils", "stuctures", "transport"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/discord-rpc/utils/RPCError.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_RPC_ERROR_CODE, RPC_ERROR_CODE } from '../structures/Transport' 2 | 3 | export class RPCError extends Error { 4 | code: RPC_ERROR_CODE | CUSTOM_RPC_ERROR_CODE 5 | message = '' 6 | 7 | get name() { 8 | return `${{ ...CUSTOM_RPC_ERROR_CODE, ...RPC_ERROR_CODE }[this.code]}` 9 | } 10 | 11 | constructor(errorCode: CUSTOM_RPC_ERROR_CODE | RPC_ERROR_CODE, message?: string, options?: any) { 12 | super(message) 13 | 14 | this.code = errorCode 15 | this.message = message ?? this.message 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/discord-rpc/utils/TypedEmitter.ts: -------------------------------------------------------------------------------- 1 | // https://www.npmjs.com/package/typed-emitter 2 | 3 | export interface TypedEmitter< 4 | eventName extends { 5 | [eventName: string]: (...args: any[]) => void 6 | }, 7 | > { 8 | addListener(event: E, listener: eventName[E]): this 9 | on(event: E, listener: eventName[E]): this 10 | once(event: E, listener: eventName[E]): this 11 | prependListener(event: E, listener: eventName[E]): this 12 | prependOnceListener(event: E, listener: eventName[E]): this 13 | 14 | off(event: E, listener: eventName[E]): this 15 | removeAllListeners(event?: E): this 16 | removeListener(event: E, listener: eventName[E]): this 17 | 18 | emit(event: E, ...args: Parameters): boolean 19 | eventNames(): (keyof eventName | string | symbol)[] 20 | rawListeners(event: E): eventName[E][] 21 | listeners(event: E): eventName[E][] 22 | listenerCount(event: E): number 23 | 24 | getMaxListeners(): number 25 | setMaxListeners(maxListeners: number): this 26 | } 27 | -------------------------------------------------------------------------------- /packages/file-transfer/README.md: -------------------------------------------------------------------------------- 1 | # Download Core 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/file-transfer.svg)](https://www.npmjs.com/package/@xmcl/file-transfer) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/file-transfer.svg)](https://npmjs.com/@xmcl/file-transfer) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/file-transfer)](https://packagephobia.now.sh/result?p=@xmcl/file-transfer) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Provide a high performance download file function based on [undici](https://github.com/nodejs/undici). 10 | 11 | - Support download by range request 12 | - Customize the range size 13 | - Support validating the checksum 14 | - If the validation matched, it won't download the file. 15 | - Also support customize validation. 16 | - Support download and fallback to another url 17 | - Support AbortSignal 18 | - Fully customizable retry logic 19 | 20 | ## Usage 21 | 22 | Download the file by url 23 | 24 | ```ts 25 | import { download } from '@xmcl/file-transfer' 26 | 27 | await download({ 28 | url: 'http://example.com/file.zip', // required 29 | destination: 'file.zip', // required 30 | headers: { // optional 31 | 'customized': 'header' 32 | }, 33 | abortSignal: new AbortController().signal, // optional 34 | progressController: (url, chunkSize, progress, total) => { // optional 35 | console.log(url) 36 | console.log(chunkSize) 37 | console.log(progress) 38 | console.log(total) 39 | }, 40 | // use validator to validate the file 41 | validator: { // optional 42 | algorithm: 'sha1', 43 | hash: '1234567890abcdef1234567890abcdef12345678', 44 | } 45 | }) 46 | ``` 47 | 48 | Download with fallback url 49 | 50 | ```ts 51 | import { download } from '@xmcl/file-transfer' 52 | 53 | await download({ 54 | // using array to fallback 55 | url: ['http://example.com/file.zip', 'http://example.com/fallback.zip'], 56 | destination: 'file.zip', 57 | }) 58 | ``` 59 | -------------------------------------------------------------------------------- /packages/file-transfer/agent.ts: -------------------------------------------------------------------------------- 1 | import { Agent, interceptors, RetryHandler } from 'undici' 2 | 3 | export function getDefaultAgent(retry?: RetryHandler.RetryOptions, defaultMaxRedirections = 5) { 4 | const options: Agent.Options = { 5 | connections: 16, 6 | } 7 | return new Agent(options).compose(interceptors.retry(retry), interceptors.redirect({ maxRedirections: defaultMaxRedirections })) 8 | } 9 | 10 | -------------------------------------------------------------------------------- /packages/file-transfer/checkpoint.ts: -------------------------------------------------------------------------------- 1 | import { FileHandle } from 'fs/promises' 2 | import CachePolicy from 'http-cache-semantics' 3 | import { Range } from './rangePolicy' 4 | 5 | export interface DownloadCheckpoint { 6 | ranges: Range[] 7 | url: string 8 | contentLength: number 9 | policy: CachePolicy 10 | } 11 | 12 | export interface CheckpointHandler { 13 | lookup(url: URL, handle: FileHandle, destination: string): Promise 14 | put(url: URL, handle: FileHandle, destination: string, checkpoint: DownloadCheckpoint): Promise 15 | delete(url: URL, handle: FileHandle, destination: string): Promise 16 | } 17 | 18 | export function createInMemoryCheckpointHandler(): CheckpointHandler { 19 | const storage: Record = {} 20 | return { 21 | async lookup(url: URL, handle: FileHandle, destination: string) { 22 | const result = storage[destination] 23 | delete storage[destination] 24 | return result 25 | }, 26 | async put(url: URL, handle: FileHandle, destination: string, checkpoint: DownloadCheckpoint) { 27 | storage[destination] = checkpoint 28 | }, 29 | async delete(url, handle, destination) { 30 | delete storage[destination] 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/file-transfer/error.ts: -------------------------------------------------------------------------------- 1 | import { Range } from './rangePolicy' 2 | 3 | export class DownloadError extends Error { 4 | constructor( 5 | message: string, 6 | public urls: string[], 7 | readonly headers: Record, 8 | readonly destination: string, 9 | options?: ErrorOptions, 10 | ) { 11 | super(message, options) 12 | this.name = 'DownloadError' 13 | } 14 | } 15 | 16 | export class DownloadFileSystemError extends DownloadError { 17 | constructor( 18 | message: string, 19 | urls: string[], 20 | headers: Record, 21 | destination: string, 22 | readonly error: unknown, 23 | ) { 24 | super( 25 | message, 26 | urls, 27 | headers, 28 | destination, 29 | { cause: error }, 30 | ) 31 | this.name = 'DownloadFileSystemError' 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/file-transfer/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @xmcl/file-transfer 3 | */ 4 | 5 | export * from './error' 6 | export * from './agent' 7 | export * from './rangePolicy' 8 | export * from './progress' 9 | export * from './retry' 10 | export * from './validator' 11 | export * from './download' 12 | -------------------------------------------------------------------------------- /packages/file-transfer/metadata.ts: -------------------------------------------------------------------------------- 1 | export function parseRangeInfo(headers: Record) { 2 | const contentLength = headers['content-length'] ? Number.parseInt(headers['content-length']) : -1 3 | const isAcceptRanges = headers['accept-ranges'] === 'bytes' 4 | return { 5 | contentLength, 6 | isAcceptRanges, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/file-transfer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/file-transfer", 3 | "version": "2.0.2", 4 | "main": "./index.ts", 5 | "description": "A high performance downloader based on undici", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=node16 --platform=node --external:http-cache-semantics --external:undici --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=node16 --platform=node --external:http-cache-semantics --external:undici --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "access": "public", 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs" 18 | }, 19 | "dependencies": { 20 | "@types/http-cache-semantics": "^4.0.1", 21 | "http-cache-semantics": "^4.1.1", 22 | "undici": "7.2.3" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 27 | }, 28 | "sideEffects": false, 29 | "author": "cijhn@hotmail.com", 30 | "keywords": [ 31 | "minecraft", 32 | "typescript", 33 | "minecraft-launcher", 34 | "nodejs", 35 | "electron" 36 | ], 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 40 | }, 41 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 42 | "devDependencies": { 43 | "@types/node": "~18.15.11", 44 | "@xmcl/oxlint-config": "workspace:^*", 45 | "esbuild": "^0.17.16", 46 | "oxlint": "^0.15.5", 47 | "typescript": "^5.3.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/file-transfer/progress.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The controller that maintain the download status 3 | */ 4 | export interface ProgressController { 5 | (url: URL, chunkSize: number, written: number, total: number): void 6 | } 7 | 8 | export function createProgressController(onProgress?: ProgressController): ProgressController { 9 | const controller: ProgressController = (url, chunk, _progress, total) => { 10 | onProgress?.(url, chunk, _progress, total) 11 | } 12 | return controller 13 | } 14 | 15 | export function resolveProgressController(controller?: ProgressController): ProgressController { 16 | if (!controller) { return createProgressController() } 17 | if (typeof controller === 'function') { return createProgressController(controller) } 18 | return controller 19 | } 20 | -------------------------------------------------------------------------------- /packages/file-transfer/rangePolicy.ts: -------------------------------------------------------------------------------- 1 | export interface Range { 2 | start: number 3 | end: number 4 | } 5 | 6 | export interface RangePolicy { 7 | computeRanges(contentLength: number): Range[] 8 | } 9 | 10 | export function isRangePolicy(rangeOptions?: RangePolicy | DefaultRangePolicyOptions): rangeOptions is RangePolicy { 11 | if (!rangeOptions) { return false } 12 | return 'computeRanges' in rangeOptions && typeof rangeOptions.computeRanges === 'function' 13 | } 14 | 15 | export function resolveRangePolicy(rangeOptions?: RangePolicy | DefaultRangePolicyOptions) { 16 | if (isRangePolicy(rangeOptions)) { 17 | return rangeOptions 18 | } 19 | return new DefaultRangePolicy(rangeOptions?.rangeThreshold ?? 1024 * 1024, 4) 20 | } 21 | 22 | export interface DefaultRangePolicyOptions { 23 | /** 24 | * The minimum bytes a range should have. 25 | * @default 2MB 26 | */ 27 | rangeThreshold?: number 28 | } 29 | 30 | export class DefaultRangePolicy implements RangePolicy { 31 | constructor( 32 | readonly rangeThreshold: number, 33 | readonly concurrency: number 34 | ) { } 35 | 36 | getConcurrency() { 37 | return this.concurrency 38 | } 39 | 40 | computeRanges(total: number): Range[] { 41 | const { rangeThreshold: minChunkSize } = this 42 | if (total <= minChunkSize) { 43 | return [{ start: 0, end: total }] 44 | } 45 | const partSize = Math.max(minChunkSize, Math.floor(total / this.getConcurrency())) 46 | const ranges: Range[] = [] 47 | for (let cur = 0, chunkSize = 0; cur < total; cur += chunkSize) { 48 | const remain = total - cur 49 | if (remain >= partSize) { 50 | chunkSize = partSize 51 | ranges.push({ start: cur, end: cur + chunkSize - 1 }) 52 | } else { 53 | const last = ranges[ranges.length - 1] 54 | if (!last) { 55 | ranges.push({ start: 0, end: remain - 1 }) 56 | } else { 57 | last.end = last.end + remain 58 | } 59 | cur = total 60 | } 61 | } 62 | return ranges 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/file-transfer/retry.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-redeclare */ 2 | import { setTimeout } from 'timers/promises' 3 | import { errors } from 'undici' 4 | import { DownloadError } from './error' 5 | import { ValidationError } from './validator' 6 | 7 | /** 8 | * The handler that decide whether 9 | */ 10 | export interface RetryPolicy { 11 | retry(url: URL, attempt: number, error: ValidationError): boolean | Promise 12 | retry(url: URL, attempt: number, error: DownloadError): boolean | Promise 13 | /** 14 | * You should decide whether we should retry the download again? 15 | * 16 | * @param url The current downloading url 17 | * @param attempt How many time it try to retry download? The first retry will be `1`. 18 | * @param error The error object thrown during this download. It can be {@link DownloadError} or ${@link ValidationError}. 19 | * @returns If we should retry and download it again. 20 | */ 21 | retry(url: URL, attempt: number, error: any): boolean | Promise 22 | } 23 | 24 | export interface DefaultRetryPolicyOptions { 25 | /** 26 | * The max retry count 27 | */ 28 | maxRetryCount?: number 29 | /** 30 | * Should we retry on the error 31 | */ 32 | shouldRetry?: (e: any) => boolean 33 | } 34 | 35 | export function isRetryHandler(options?: DefaultRetryPolicyOptions | RetryPolicy): options is RetryPolicy { 36 | if (!options) { return false } 37 | return 'retry' in options && typeof options.retry === 'function' 38 | } 39 | 40 | export function resolveRetryHandler(options?: DefaultRetryPolicyOptions | RetryPolicy): RetryPolicy { 41 | if (isRetryHandler(options)) { return options } 42 | return createDefaultRetryHandler(options?.maxRetryCount ?? 3) 43 | } 44 | 45 | const kHandled = Symbol('Handled') 46 | export function createDefaultRetryHandler(maxRetryCount = 3) { 47 | const handler: RetryPolicy = { 48 | async retry(url, attempt, error) { 49 | if (error[kHandled]) { 50 | await setTimeout(error[kHandled]) 51 | return true 52 | } 53 | if (attempt < maxRetryCount) { 54 | if (error instanceof errors.HeadersTimeoutError || 55 | error instanceof errors.BodyTimeoutError || 56 | error instanceof (errors as any).ConnectTimeoutError || 57 | error instanceof errors.SocketError) { 58 | const timeout = (attempt + 1) * 1000 59 | Object.defineProperty(error, kHandled, { value: timeout, enumerable: false }) 60 | await setTimeout(timeout) 61 | return true 62 | } 63 | } 64 | return false 65 | }, 66 | } 67 | return handler 68 | } 69 | -------------------------------------------------------------------------------- /packages/file-transfer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "agent.ts", 4 | "checkpoint.ts", 5 | "error.ts", 6 | "index.ts", 7 | "metadata.ts", 8 | "download.ts", 9 | "retry.ts", 10 | "rangePolicy.ts", 11 | "progress.ts", 12 | "validator.ts" 13 | ], 14 | "extends": "../../tsconfig.json", 15 | "compilerOptions": { 16 | "outDir": "./dist" 17 | }, 18 | } -------------------------------------------------------------------------------- /packages/file-transfer/validator.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto' 2 | import { readFile, createReadStream } from 'fs' 3 | import { promisify } from 'util' 4 | import { pipeline } from 'stream/promises' 5 | 6 | export interface Validator { 7 | /** 8 | * Validate the download result. It should throw `ValidationError` if validation failed. 9 | * 10 | * @param destination The result file 11 | * @param url The url where the file downloaded from 12 | */ 13 | validate(destination: string, url: string): Promise 14 | } 15 | 16 | export class ChecksumValidator implements Validator { 17 | constructor(protected checksum?: ChecksumValidatorOptions) { } 18 | 19 | async validate(destination: string, url: string): Promise { 20 | if (this.checksum) { 21 | const checksum = this.checksum 22 | const hash = createHash(checksum.algorithm) 23 | await pipeline(createReadStream(destination), hash).catch((e) => { 24 | if (e.code === 'ENOENT') { 25 | throw new ChecksumNotMatchError(checksum.algorithm, checksum.hash, '', destination, url) 26 | } 27 | throw e 28 | }) 29 | const actual = hash.digest('hex') 30 | const expect = checksum.hash 31 | if (actual !== expect) { 32 | throw new ChecksumNotMatchError(checksum.algorithm, checksum.hash, actual, destination, url) 33 | } 34 | } 35 | } 36 | } 37 | 38 | export function isValidator(options?: Validator | ChecksumValidatorOptions): options is Validator { 39 | if (!options) { return false } 40 | return 'validate' in options && typeof options.validate === 'function' 41 | } 42 | 43 | export function resolveValidator(options?: ChecksumValidatorOptions | Validator): Validator | undefined { 44 | if (isValidator(options)) { return options } 45 | if (options) { 46 | return new ChecksumValidator({ hash: options.hash, algorithm: options.algorithm }) 47 | } 48 | return undefined 49 | } 50 | 51 | export interface ChecksumValidatorOptions { 52 | algorithm: string 53 | hash: string 54 | } 55 | 56 | export class JsonValidator implements Validator { 57 | async validate(destination: string, url: string): Promise { 58 | const content = await promisify(readFile)(destination, 'utf-8') 59 | JSON.parse(content) 60 | } 61 | } 62 | 63 | export class ValidationError extends Error { 64 | constructor(error: string, message?: string) { 65 | super(message) 66 | this.name = error 67 | } 68 | } 69 | 70 | export class ChecksumNotMatchError extends ValidationError { 71 | constructor(readonly algorithm: string, readonly expect: string, readonly actual: string, readonly file: string, readonly source?: string) { 72 | super('ChecksumNotMatchError', source ? `File ${file} (${source}) ${algorithm} checksum not match. Expect: ${expect}. Actual: ${actual}.` : `File ${file} ${algorithm} checksum not match. Expect: ${expect}. Actual: ${actual}.`) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/forge-site-parser/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/forge-site-parser/README.md: -------------------------------------------------------------------------------- 1 | # Forge Site Parser 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/forge-installer.svg)](https://www.npmjs.com/package/@xmcl/forge-installer) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/forge-installer.svg)](https://npmjs.com/@xmcl/forge-installer) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/forge-installer)](https://packagephobia.now.sh/result?p=@xmcl/forge-installer) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Provide a parser to parse forge website (files.minecraftforge.net). 10 | 11 | For detail please see the [github repository](https://github.com/Voxelum/minecraft-launcher-core-node). 12 | -------------------------------------------------------------------------------- /packages/forge-site-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/forge-site-parser", 3 | "version": "2.0.9", 4 | "main": "./index.ts", 5 | "description": "A MinecraftForge web page parser", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=node16 --platform=node --external:node-html-parser --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=node16 --platform=node --external:node-html-parser --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "access": "public", 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs" 18 | }, 19 | "dependencies": { 20 | "node-html-parser": "^6.1.5" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 25 | }, 26 | "author": "cijhn@hotmail.com", 27 | "keywords": [ 28 | "minecraft", 29 | "typescript", 30 | "minecraft-launcher", 31 | "nodejs", 32 | "electron", 33 | "forge", 34 | "minecraftforge", 35 | "liteloader", 36 | "fabric" 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 41 | }, 42 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 43 | "devDependencies": { 44 | "@types/node": "~18.15.11", 45 | "@xmcl/oxlint-config": "workspace:^*", 46 | "esbuild": "^0.17.16", 47 | "oxlint": "^0.15.5", 48 | "typescript": "^5.3.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/forge-site-parser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts" 4 | ], 5 | "extends": "../../tsconfig.json", 6 | "compilerOptions": { 7 | "outDir": "./dist", 8 | }, 9 | } -------------------------------------------------------------------------------- /packages/game-data/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/game-data/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @xmcl/game-data 3 | */ 4 | export * from './level' 5 | export * from './serverDat' 6 | -------------------------------------------------------------------------------- /packages/game-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/game-data", 3 | "version": "1.2.5", 4 | "main": "./index.ts", 5 | "description": "Parse minecraft game related data like level.dat, server.dat", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=es2020 --external:@xmcl/* --platform=neutral --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=es2020 --external:@xmcl/* --platform=neutral --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "main": "./dist/index.js", 16 | "module": "./dist/index.mjs", 17 | "access": "public" 18 | }, 19 | "dependencies": { 20 | "@xmcl/nbt": "workspace:^*", 21 | "@xmcl/system": "workspace:^*" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 26 | }, 27 | "author": "cijhn@hotmail.com", 28 | "keywords": [ 29 | "minecraft", 30 | "typescript", 31 | "minecraft-launcher", 32 | "nodejs", 33 | "electron", 34 | "chunk", 35 | "minecraft-level" 36 | ], 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 40 | }, 41 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 42 | "devDependencies": { 43 | "@types/node": "~18.15.11", 44 | "@xmcl/oxlint-config": "workspace:^*", 45 | "tslib": "^2.5.0", 46 | "esbuild": "^0.17.16", 47 | "oxlint": "^0.15.5", 48 | "typescript": "^5.3.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/game-data/serverDat.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { describe, expect, test } from 'vitest' 3 | import { readServerInfo, readServerInfoSync, writeServerInfo, writeServerInfoSync } from './index' 4 | 5 | describe('Server Info', () => { 6 | test('should read server.dat file', async ({ mock }) => { 7 | const data = readFileSync(`${mock}/servers.dat`) 8 | const infos = await readServerInfo(data) 9 | expect(infos[0].name).toEqual('nyaacat') 10 | expect(infos[1].name).toEqual('himajin') 11 | expect(infos[2].name).toEqual('mcJp') 12 | expect(infos[3].name).toEqual('Minecraft Server') 13 | }) 14 | test('should sync read server.dat file', async ({ mock }) => { 15 | const data = readFileSync(`${mock}/servers.dat`) 16 | const infos = readServerInfoSync(data) 17 | expect(infos[0].name).toEqual('nyaacat') 18 | expect(infos[1].name).toEqual('himajin') 19 | expect(infos[2].name).toEqual('mcJp') 20 | expect(infos[3].name).toEqual('Minecraft Server') 21 | }) 22 | test('should write to nbt data right', async () => { 23 | const byte = await writeServerInfo([ 24 | { 25 | name: 'abc', 26 | ip: 'ip!', 27 | icon: '', 28 | acceptTextures: 0, 29 | }, 30 | ]) 31 | const readBack = await readServerInfo(byte) 32 | expect(readBack[0]).toBeTruthy() 33 | expect(readBack[0].name).toEqual('abc') 34 | expect(readBack[0].ip).toEqual('ip!') 35 | }) 36 | test('should sync write to nbt data right', async () => { 37 | const byte = writeServerInfoSync([ 38 | { 39 | name: 'abc', 40 | ip: 'ip!', 41 | icon: '', 42 | acceptTextures: 0, 43 | }, 44 | ]) 45 | const readBack = await readServerInfo(byte) 46 | expect(readBack[0]).toBeTruthy() 47 | expect(readBack[0].name).toEqual('abc') 48 | expect(readBack[0].ip).toEqual('ip!') 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /packages/game-data/serverDat.ts: -------------------------------------------------------------------------------- 1 | import { deserialize, deserializeSync, serialize, serializeSync, TagType } from '@xmcl/nbt' 2 | 3 | export class ServerInfo { 4 | @TagType(TagType.String) 5 | icon = '' 6 | 7 | @TagType(TagType.String) 8 | ip = '' 9 | 10 | @TagType(TagType.String) 11 | name = '' 12 | 13 | @TagType(TagType.Byte) 14 | acceptTextures = 0 15 | } 16 | 17 | /** 18 | * The servers.dat format server information, contains known host displayed in "Multipler" page. 19 | */ 20 | export class ServersData { 21 | @TagType([ServerInfo]) 22 | servers: ServerInfo[] = [] 23 | } 24 | 25 | /** 26 | * Read the server information from the binary data of .minecraft/server.dat file, which stores the local known server host information. 27 | * 28 | * @param buff The binary data of .minecraft/server.dat 29 | */ 30 | export async function readServerInfo(buff: Uint8Array): Promise { 31 | const value = await deserialize(buff, { type: ServersData }) 32 | return value.servers 33 | } 34 | 35 | /** 36 | * Write the information to NBT format used by .minecraft/server.dat file. 37 | * 38 | * @param infos The array of server information. 39 | */ 40 | export function writeServerInfo(infos: ServerInfo[]): Promise { 41 | const tag = new ServersData() 42 | tag.servers = infos 43 | return serialize(tag) 44 | } 45 | 46 | /** 47 | * Read the server information from the binary data of .minecraft/server.dat file, which stores the local known server host information. 48 | * 49 | * @param buff The binary data of .minecraft/server.dat 50 | */ 51 | export function readServerInfoSync(buff: Uint8Array): ServerInfo[] { 52 | const value = deserializeSync(buff, { type: ServersData }) 53 | return value.servers 54 | } 55 | 56 | /** 57 | * Write the information to NBT format used by .minecraft/server.dat file. 58 | * 59 | * @param infos The array of server information. 60 | */ 61 | export function writeServerInfoSync(infos: ServerInfo[]): Uint8Array { 62 | const tag = new ServersData() 63 | tag.servers = infos 64 | return serializeSync(tag) 65 | } 66 | -------------------------------------------------------------------------------- /packages/game-data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "level.ts", 4 | "serverDat.ts", 5 | "index.ts" 6 | ], 7 | "extends": "../../tsconfig.json", 8 | "compilerOptions": { 9 | "outDir": "./dist" 10 | }, 11 | } -------------------------------------------------------------------------------- /packages/gamesetting/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/gamesetting/README.md: -------------------------------------------------------------------------------- 1 | # Gamesetting Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/gamesetting.svg)](https://www.npmjs.com/package/@xmcl/gamesetting) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/gamesetting.svg)](https://npmjs.com/@xmcl/gamesetting) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/gamesetting)](https://packagephobia.now.sh/result?p=@xmcl/gamesetting) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Provide function to parse Minecraft game settings 10 | 11 | ## Usage 12 | 13 | ### Parse GameSetting (options.txt) 14 | 15 | Serialize/Deserialize the minecraft game setting string. 16 | 17 | ```ts 18 | import { GameSetting } from '@xmcl/gamesetting' 19 | const settingString; 20 | const setting: GameSetting = GameSetting.parse(settingString); 21 | const string: string = GameSetting.stringify(setting); 22 | ``` 23 | -------------------------------------------------------------------------------- /packages/gamesetting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/gamesetting", 3 | "version": "3.0.3", 4 | "main": "./index.ts", 5 | "description": "Parse Minecraft gamesetting (options.txt) with type.", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "publishConfig": { 10 | "access": "public", 11 | "main": "./dist/index.js", 12 | "module": "./dist/index.mjs" 13 | }, 14 | "scripts": { 15 | "build:type": "tsc", 16 | "build:cjs": "esbuild --target=es2020 --platform=neutral --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 17 | "build:esm": "esbuild --target=es2020 --platform=neutral --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 22 | }, 23 | "author": "cijhn@hotmail.com", 24 | "keywords": [ 25 | "minecraft", 26 | "typescript", 27 | "minecraft-launcher", 28 | "nodejs", 29 | "electron" 30 | ], 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 34 | }, 35 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 36 | "devDependencies": { 37 | "@types/node": "~18.15.11", 38 | "@xmcl/oxlint-config": "workspace:^*", 39 | "esbuild": "^0.17.16", 40 | "oxlint": "^0.15.5", 41 | "typescript": "^5.3.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/gamesetting/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts" 4 | ], 5 | "extends": "../../tsconfig.json", 6 | "compilerOptions": { 7 | "outDir": "./dist" 8 | }, 9 | } -------------------------------------------------------------------------------- /packages/installer/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/installer/diagnose.ts: -------------------------------------------------------------------------------- 1 | import { diagnoseFile, Issue, LibraryIssue, MinecraftFolder, MinecraftLocation, Version } from '@xmcl/core' 2 | import { InstallProfile, resolveProcessors, PostProcessor } from './profile' 3 | 4 | export type InstallIssues = ProcessorIssue | LibraryIssue 5 | 6 | /** 7 | * The processor issue 8 | */ 9 | export interface ProcessorIssue extends Issue { 10 | role: 'processor' 11 | 12 | /** 13 | * The processor 14 | */ 15 | processor: PostProcessor 16 | } 17 | 18 | export interface InstallProfileIssueReport { 19 | minecraftLocation: MinecraftFolder 20 | installProfile: InstallProfile 21 | issues: InstallIssues[] 22 | } 23 | 24 | /** 25 | * Diagnose a install profile status. Check if it processor output correctly processed. 26 | * 27 | * This can be used for check if forge correctly installed when minecraft >= 1.13 28 | * @beta 29 | * 30 | * @param installProfile The install profile. 31 | * @param minecraftLocation The minecraft location 32 | */ 33 | export async function diagnoseInstall(installProfile: InstallProfile, minecraftLocation: MinecraftLocation, side: 'client' | 'server' = 'client'): Promise { 34 | const mc = MinecraftFolder.from(minecraftLocation) 35 | const report: InstallProfileIssueReport = { 36 | minecraftLocation: mc, 37 | installProfile, 38 | issues: [], 39 | } 40 | const issues = report.issues 41 | const processors: PostProcessor[] = resolveProcessors(side, installProfile, mc) 42 | await Promise.all(Version.resolveLibraries(installProfile.libraries).map(async (lib) => { 43 | const libPath = mc.getLibraryByPath(lib.download.path) 44 | const issue = await diagnoseFile({ 45 | role: 'library', 46 | file: libPath, 47 | expectedChecksum: lib.download.sha1, 48 | hint: 'Problem on install_profile! Please consider to use Installer.installByProfile to fix.', 49 | }) 50 | if (issue) { 51 | issues.push(Object.assign(issue, { library: lib })) 52 | } 53 | })) 54 | for (const proc of processors) { 55 | if (proc.outputs) { 56 | for (const [file, checksum] of Object.entries(proc.outputs)) { 57 | const issue = await diagnoseFile({ 58 | role: 'processor', 59 | file, 60 | expectedChecksum: checksum.replace(/'/g, ''), 61 | hint: 'Re-install this installer profile!', 62 | }) 63 | if (issue) { 64 | issues.push(Object.assign(issue, { processor: proc })) 65 | } 66 | } 67 | } 68 | } 69 | return report 70 | } 71 | -------------------------------------------------------------------------------- /packages/installer/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The installer module provides commonly used installation functions for Minecraft. 3 | * @packageDocumentation 4 | * @module @xmcl/installer 5 | */ 6 | 7 | export * from './fabric' 8 | export * from './liteloader' 9 | export * from './forge' 10 | export * from './neoForged' 11 | export * from './minecraft' 12 | export * from './profile' 13 | export * from './optifine' 14 | export * from './java' 15 | export * from './java-runtime' 16 | export * from './diagnose' 17 | export * from './quilt' 18 | export { InstallOptions } from './utils' 19 | export * from './downloadTask' 20 | export * from './unzip' 21 | export * from './labymod' 22 | -------------------------------------------------------------------------------- /packages/installer/java.test.ts: -------------------------------------------------------------------------------- 1 | import { parseJavaVersion } from './java' 2 | import { describe, test, expect } from 'vitest' 3 | 4 | describe('JavaInstaller', () => { 5 | describe('#parseJavaVersion', () => { 6 | test('should resolve old java version', async () => { 7 | const version = `java version "1.7.0_55" 8 | Java(TM) SE Runtime Environment (build 1.7.0_55-b13) 9 | Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)` 10 | const inf = parseJavaVersion(version) 11 | expect(inf).toEqual({ version: '1.7.0_55', majorVersion: 7, patch: 55 }) 12 | }) 13 | test('should resolve new java version', async () => { 14 | const version = `java 10.0.1 2018-04-17 15 | Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10) 16 | Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)` 17 | const inf = parseJavaVersion(version) 18 | expect(inf).toEqual({ version: '10.0.1', majorVersion: 10, patch: 1 }) 19 | }) 20 | test('should return undefined if version is not valid', async () => { 21 | const version = 'java aaaa 2018-04-17' 22 | const inf = parseJavaVersion(version) 23 | expect(inf).toEqual(undefined) 24 | }) 25 | test('should parse', () => { 26 | const inf = parseJavaVersion(`openjdk version "1.8.0-262" 27 | OpenJDK Runtime Environment (build 1.8.0-262-b10) 28 | OpenJDK 64-Bit Server VM (build 25.71-b10, mixed mode)`) 29 | expect(inf).toEqual({ version: '1.8.0', majorVersion: 8, patch: -1 }) 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/installer/manifest.ts: -------------------------------------------------------------------------------- 1 | export function convertClasspathToMaven(paths: string[]): string[] { 2 | return paths.map(path => { 3 | const trimmedPath = path.replace(/^libraries\//, '') 4 | 5 | const parts = trimmedPath.split('/') 6 | 7 | let jarName = parts.pop() 8 | const version = parts.pop() 9 | const artifactId = parts.pop() 10 | const groupIdParts = parts 11 | 12 | const groupId = groupIdParts.join('.') 13 | 14 | let classifier = '' 15 | 16 | if (jarName) { 17 | jarName = jarName.replace(/\.jar$/, '') 18 | const jarParts = jarName?.substring(`${artifactId}-${version}`.length + 1).split('-') 19 | if (jarParts && jarParts.length > 0) { 20 | classifier = jarParts[0] 21 | } 22 | } 23 | 24 | let mavenCoordinate = `${groupId}:${artifactId}:${version}` 25 | if (classifier) { 26 | mavenCoordinate += `:${classifier}` 27 | } 28 | 29 | return mavenCoordinate 30 | }) 31 | } 32 | 33 | export function parseManifest(manifestContent: string): { mainClass: string; classPath: string[] } { 34 | const lines = manifestContent.split('\r\n') 35 | let mainClass = '' 36 | let classPath = [] as string[] 37 | 38 | for (let i = 0; i < lines.length; i++) { 39 | const line = lines[i].trim() 40 | if (line.startsWith('Main-Class:')) { 41 | mainClass = line.substring('Main-Class:'.length).trim() 42 | } else if (line.startsWith('Class-Path:')) { 43 | let classPathLine = line.substring('Class-Path:'.length).trim() 44 | while (i + 1 < lines.length && lines[i + 1].startsWith(' ')) { 45 | i++ 46 | classPathLine += lines[i].slice(1) 47 | } 48 | classPath = classPathLine.split(' ').filter(path => path.length > 0) 49 | } 50 | } 51 | 52 | return { 53 | mainClass, 54 | classPath, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/installer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/installer", 3 | "version": "6.1.0", 4 | "main": "./index.ts", 5 | "description": "The installers of Minecraft/Forge/Fabric/Liteloader/Quilt", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=node16 --platform=node --external:@xmcl/* --external:undici --external:yauzl --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=node16 --platform=node --external:@xmcl/* --external:undici --external:yauzl --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "access": "public", 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs" 18 | }, 19 | "dependencies": { 20 | "@xmcl/asm": "workspace:^*", 21 | "@xmcl/core": "workspace:^*", 22 | "@xmcl/file-transfer": "workspace:^*", 23 | "@xmcl/forge-site-parser": "workspace:^*", 24 | "@xmcl/task": "workspace:^*", 25 | "@xmcl/unzip": "workspace:^*", 26 | "undici": "7.2.3", 27 | "yazl": "^2.5.1", 28 | "yauzl": "^2.10.0" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 33 | }, 34 | "sideEffects": false, 35 | "author": "cijhn@hotmail.com", 36 | "keywords": [ 37 | "minecraft", 38 | "typescript", 39 | "minecraft-launcher", 40 | "nodejs", 41 | "electron" 42 | ], 43 | "license": "MIT", 44 | "bugs": { 45 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 46 | }, 47 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 48 | "devDependencies": { 49 | "@types/node": "~18.15.11", 50 | "@types/yazl": "^2.4.5", 51 | "@types/yauzl": "^2.10.0", 52 | "@xmcl/oxlint-config": "workspace:^*", 53 | "esbuild": "^0.17.16", 54 | "oxlint": "^0.15.5", 55 | "typescript": "^5.3.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/installer/quilt.ts: -------------------------------------------------------------------------------- 1 | import { MinecraftFolder, MinecraftLocation, Version } from '@xmcl/core' 2 | import { writeFile } from 'fs/promises' 3 | import { FetchOptions, doFetch, ensureFile } from './utils' 4 | import { FabricArtifactVersion, FabricLoaderArtifact } from './fabric' 5 | 6 | export const DEFAULT_META_URL_QUILT = 'https://meta.quiltmc.org' 7 | 8 | export interface GetQuiltOptions extends FetchOptions { 9 | minecraftVersion: string 10 | } 11 | 12 | export interface QuiltLoaderArtifact extends FabricLoaderArtifact { 13 | hashed: FabricLoaderArtifact['intermediary'] 14 | } 15 | 16 | /** 17 | * Get supported fabric game versions 18 | */ 19 | export async function getQuiltGames(options?: FetchOptions): Promise { 20 | const response = await doFetch(options, `${DEFAULT_META_URL_QUILT}/v3/game`) 21 | const body = await response.json() as Array<{ version: string }> 22 | return body.map((g) => g.version) 23 | } 24 | 25 | /** 26 | * Get quilt-loader artifact list 27 | */ 28 | export async function getQuiltLoaders(options?: FetchOptions): Promise { 29 | const response = await doFetch(options, `${DEFAULT_META_URL_QUILT}/v3/versions/loader`) 30 | const body = response.json() 31 | return body 32 | } 33 | 34 | /** 35 | * Get quilt loader versions list for a specific minecraft version 36 | */ 37 | export async function getQuiltLoaderVersionsByMinecraft(options: GetQuiltOptions): Promise { 38 | const response = await doFetch(options, `${DEFAULT_META_URL_QUILT}/v3/versions/loader/${options.minecraftVersion}`) 39 | const content: QuiltLoaderArtifact[] = await response.json() 40 | return content 41 | } 42 | 43 | export interface InstallQuiltVersionOptions extends FetchOptions { 44 | minecraftVersion: string 45 | version: string 46 | minecraft: MinecraftLocation 47 | side?: 'client' | 'server' 48 | } 49 | 50 | /** 51 | * Install quilt version via profile API 52 | */ 53 | export async function installQuiltVersion(options: InstallQuiltVersionOptions) { 54 | const side = options.side ?? 'client' 55 | const url = side === 'client' 56 | ? `${DEFAULT_META_URL_QUILT}/v3/versions/loader/${options.minecraftVersion}/${options.version}/profile/json` 57 | : `${DEFAULT_META_URL_QUILT}/v3/versions/loader/${options.minecraftVersion}/${options.version}/server/json` 58 | const response = await doFetch(options, url) 59 | const content: Version = await response.json() as any 60 | 61 | const minecraft = MinecraftFolder.from(options.minecraft) 62 | const versionName = `${options.minecraftVersion}-quilt${options.version}` 63 | content.id = versionName 64 | 65 | const jsonPath = side === 'client' ? minecraft.getVersionJson(versionName) : minecraft.getVersionServerJson(versionName) 66 | 67 | await ensureFile(jsonPath) 68 | await writeFile(jsonPath, JSON.stringify(content)) 69 | 70 | return versionName 71 | } 72 | -------------------------------------------------------------------------------- /packages/installer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts", 4 | "minecraft.ts", 5 | "forge.ts", 6 | "liteloader.ts", 7 | "java-runtime.ts", 8 | "optifine.ts", 9 | "fabric.ts", 10 | "quilt.ts", 11 | "utils.ts", 12 | "unzip.ts", 13 | "java.ts", 14 | "diagnose.ts", 15 | "profile.ts", 16 | "labymod.ts", 17 | "manifest.ts", 18 | "downloadTask.ts" 19 | ], 20 | "extends": "../../tsconfig.json", 21 | "compilerOptions": { 22 | "outDir": "./dist" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/installer/zipValdiator.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError, Validator } from '@xmcl/file-transfer' 2 | import { open } from '@xmcl/unzip' 3 | 4 | export class ZipValidator implements Validator { 5 | async validate(destination: string, url: string): Promise { 6 | try { 7 | const file = await open(destination, { autoClose: false, lazyEntries: true }) 8 | file.close() 9 | } catch (e) { 10 | throw new ValidationError('InvalidZipError', (e as any).message) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/mod-parser/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/mod-parser/fabric.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { readFabricMod } from './fabric' 3 | import { describe, test, expect } from 'vitest' 4 | import { openFileSystem } from '@xmcl/system' 5 | 6 | describe('Fabric', () => { 7 | describe('#readFabricMod', () => { 8 | test('should read the simple fabric json', async ({ mock }) => { 9 | const mod = await readFabricMod(path.join(mock, 'mods', 'fabric-sample.jar')) 10 | expect(mod).toBeTruthy() 11 | expect(mod).toEqual({ 12 | schemaVersion: 1, 13 | id: 'appleskin', 14 | version: '1.0.8', 15 | name: 'AppleSkin', 16 | description: 'Adds various food-related HUD improvements', 17 | authors: ['squeek502'], 18 | contact: 19 | { 20 | sources: 'https://github.com/squeek502/AppleSkin', 21 | homepage: 'https://minecraft.curseforge.com/projects/appleskin', 22 | issues: 'https://github.com/squeek502/AppleSkin/issues', 23 | }, 24 | license: 'Unlicense', 25 | icon: 'assets/appleskin/appleskin.png', 26 | environment: '*', 27 | entrypoints: { client: ['squeek.appleskin.AppleSkin'] }, 28 | mixins: ['appleskin.mixins.json'], 29 | depends: { fabricloader: '>=0.4.0', fabric: '*' }, 30 | }) 31 | }) 32 | test('should not close if the fs is the input', async ({ mock }) => { 33 | const fs = await openFileSystem(path.join(mock, 'mods', 'fabric-sample.jar')) 34 | const mod = await readFabricMod(fs) 35 | expect(fs.isClosed()).toBe(false) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/mod-parser/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @xmcl/mod-parser 3 | */ 4 | export * from './forge' 5 | export * from './forgeConfig' 6 | export * from './liteloader' 7 | export * from './fabric' 8 | export * from './quilt' 9 | -------------------------------------------------------------------------------- /packages/mod-parser/liteloader.test.ts: -------------------------------------------------------------------------------- 1 | import { readLiteloaderMod } from './liteloader' 2 | import { describe, test, expect } from 'vitest' 3 | 4 | describe('Liteloader', () => { 5 | describe('#meta', () => { 6 | test('should not be able to read other file', async ({ mock }) => { 7 | await expect(readLiteloaderMod(`${mock}/mods/sample-mod.jar`)) 8 | .rejects 9 | .toHaveProperty('name', 'IllegalInputType') 10 | await expect(readLiteloaderMod(`${mock}/saves/sample-map.zip`)) 11 | .rejects 12 | .toHaveProperty('name', 'IllegalInputType') 13 | await expect(readLiteloaderMod(`${mock}/resourcepacks/sample-resourcepack.zip`)) 14 | .rejects 15 | .toHaveProperty('name', 'IllegalInputType') 16 | await expect(readLiteloaderMod(`${mock}/not-exist.zip`)) 17 | .rejects 18 | .toBeTruthy() 19 | }) 20 | test('should be able to parse liteloader info', async ({ mock }) => { 21 | const metadata = await readLiteloaderMod(`${mock}/mods/sample-mod.litemod`) 22 | if (!metadata) { throw new Error('Should not happen') } 23 | expect(metadata.name).toEqual('ArmorsHUDRevived') 24 | expect(metadata.mcversion).toEqual('1.12.r2') 25 | expect(metadata.revision).toEqual(143) 26 | expect(metadata.author).toEqual('Shadow_Hawk') 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/mod-parser/liteloader.ts: -------------------------------------------------------------------------------- 1 | import { FileSystem, resolveFileSystem } from '@xmcl/system' 2 | 3 | export interface LiteloaderModMetadata { 4 | readonly mcversion: string 5 | readonly name: string 6 | readonly revision: number 7 | 8 | readonly author?: string 9 | readonly version?: string 10 | readonly description?: string 11 | readonly url?: string 12 | 13 | readonly tweakClass?: string 14 | readonly dependsOn?: string[] 15 | readonly injectAt?: string 16 | readonly requiredAPIs?: string[] 17 | readonly classTransformerClasses?: string[] 18 | } 19 | 20 | export async function readLiteloaderMod(mod: string | Uint8Array | FileSystem) { 21 | const fs = await resolveFileSystem(mod) 22 | 23 | try { 24 | const text = await fs.readFile('litemod.json', 'utf-8').then((s) => s.replace(/^\uFEFF/, '')).catch(() => undefined) 25 | if (!text) { 26 | throw Object.assign(new Error('Illegal input type! Expect a jar file contains litemod.json'), { 27 | mod, 28 | name: 'IllegalInputType', 29 | }) 30 | } 31 | const metadata = JSON.parse(text.trim(), (key, value) => key === 'revision' ? Number.parseInt(value, 10) : value) as LiteloaderModMetadata 32 | if (!metadata.version) { 33 | (metadata as any).version = `${metadata.mcversion}:${metadata.revision || 0}` 34 | } 35 | return metadata 36 | } finally { 37 | if (fs !== mod) fs.close() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/mod-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/mod-parser", 3 | "version": "3.4.1", 4 | "main": "./index.ts", 5 | "description": "The utilities to parse Forge/Liteloader/Fabric/Quilt mod metadata.", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=node16 --platform=node --sourcemap --external:toml --external:@xmcl/* --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=node16 --platform=node --sourcemap --external:toml --external:@xmcl/* --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "access": "public", 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs" 18 | }, 19 | "dependencies": { 20 | "@xmcl/asm": "workspace:^*", 21 | "@xmcl/system": "workspace:^*", 22 | "smol-toml": "^1.3.1" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 27 | }, 28 | "author": "cijhn@hotmail.com", 29 | "sideEffects": false, 30 | "keywords": [ 31 | "minecraft", 32 | "typescript", 33 | "minecraft-launcher", 34 | "nodejs", 35 | "electron", 36 | "forge", 37 | "minecraftforge", 38 | "fabric", 39 | "liteloader", 40 | "quilt" 41 | ], 42 | "license": "MIT", 43 | "bugs": { 44 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 45 | }, 46 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 47 | "devDependencies": { 48 | "@types/node": "~18.15.11", 49 | "@xmcl/oxlint-config": "workspace:^*", 50 | "esbuild": "^0.17.16", 51 | "oxlint": "^0.15.5", 52 | "typescript": "^5.3.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/mod-parser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "forge.ts", 4 | "forgeConfig.ts", 5 | "fabric.ts", 6 | "liteloader.ts", 7 | "quilt.ts", 8 | "index.ts" 9 | ], 10 | "extends": "../../tsconfig.json", 11 | "compilerOptions": { 12 | "outDir": "./dist" 13 | }, 14 | } -------------------------------------------------------------------------------- /packages/model/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/model/README.md: -------------------------------------------------------------------------------- 1 | # Model Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/model.svg)](https://www.npmjs.com/package/@xmcl/model) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/model.svg)](https://npmjs.com/@xmcl/model) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/model)](https://packagephobia.now.sh/result?p=@xmcl/model) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | [![stability-experimental](https://img.shields.io/badge/stability-experimental-orange.svg)](https://github.com/emersion/stability-badges#experimental) 9 | 10 | *This module can only used for browser environment* 11 | 12 | ## Usage 13 | 14 | ### Build THREE.js model for block and player 15 | 16 | *Please read how to use [resourcepacks](https://github.com/Voxelum/minecraft-launcher-core-node/tree/master/packages/resourcepack/README.md) before this* 17 | 18 | Create THREE.js block model: 19 | 20 | ```ts 21 | import { BlockModelFactory } from "@xmcl/model"; 22 | 23 | const textureRegistry: TextureRegistry; 24 | 25 | const factory = new BlockModelFactory(textureRegistry); 26 | const model: BlockModel.Resolved; 27 | const o3d: THREE.Object3D = factory.getObject(model); 28 | // add o3d to your three scene 29 | ``` 30 | 31 | Create THREE.js player model: 32 | 33 | ```ts 34 | import { PlayerModel } from "@xmcl/model"; 35 | 36 | const player: PlayerModel = new PlayerModel(); 37 | const isSlimSkin: boolean; // if this skin use alex model 38 | player.setSkin("http://your-skin-url", isSlimSkin); 39 | 40 | const o3d: THREE.Object3D = player.playerObject3d; 41 | // add o3d to your three scene 42 | ``` -------------------------------------------------------------------------------- /packages/model/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @xmcl/model 3 | */ 4 | export * from './block' 5 | export * from './player' 6 | -------------------------------------------------------------------------------- /packages/model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/model", 3 | "version": "2.0.4", 4 | "main": "./index.ts", 5 | "description": "Create Three.js model for player and block", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:esm": "esbuild --target=node16 --platform=browser --external:three --external:@xmcl/* --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 12 | }, 13 | "publishConfig": { 14 | "access": "public", 15 | "module": "./dist/index.mjs" 16 | }, 17 | "dependencies": { 18 | "@types/three": "^0.150.0", 19 | "@xmcl/resourcepack": "workspace:^*", 20 | "three": "0.156.1" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 25 | }, 26 | "sideEffects": false, 27 | "author": "cijhn@hotmail.com", 28 | "keywords": [ 29 | "minecraft", 30 | "typescript", 31 | "minecraft-launcher", 32 | "nodejs", 33 | "electron" 34 | ], 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 38 | }, 39 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 40 | "devDependencies": { 41 | "@types/node": "~18.15.11", 42 | "@xmcl/oxlint-config": "workspace:^*", 43 | "esbuild": "^0.17.16", 44 | "oxlint": "^0.15.5", 45 | "typescript": "^5.3.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts", 4 | "player.ts", 5 | "player-model.ts", 6 | "block.ts" 7 | ], 8 | "extends": "../../tsconfig.json", 9 | "compilerOptions": { 10 | "lib": ["DOM"], 11 | "outDir": "./dist", 12 | "skipLibCheck": true 13 | }, 14 | } -------------------------------------------------------------------------------- /packages/modrinth/README.md: -------------------------------------------------------------------------------- 1 | # Modrinth API 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/modrinth.svg)](https://www.npmjs.com/package/@xmcl/modrinth) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/modrinth.svg)](https://npmjs.com/@xmcl/modrinth) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/modrinth)](https://packagephobia.now.sh/result?p=@xmcl/modrinth) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Provide the modrinth described in https://docs.modrinth.com/api-spec 10 | 11 | ## Usage 12 | 13 | This package is depending on undici for HTTP in nodejs, and the browser version will use browser `fetch` instead of undici. 14 | 15 | ### Search Project in Modrinth 16 | 17 | You can use keyword to search 18 | 19 | ```ts 20 | import { ModrinthV2Client, SearchResult } from '@xmcl/modrinth' 21 | const client = new ModrinthV2Client() 22 | const searchOptions: SearchOptions = { 23 | query: "shader", // searching shader 24 | }; 25 | const result: SearchResult = await client.searchProjects(settingString); 26 | const totalProjectCounts = result.total_hits; 27 | for (const project of result.hits) { 28 | console.log(`${project.project_id} ${project.title} ${project.description}`); // print project info 29 | } 30 | ``` 31 | 32 | ### Get Project in Modrinth 33 | 34 | You can get project detail info via project id, including the download url 35 | 36 | ```ts 37 | import { ModrinthV2Client, ProjectVersionFile, ProjectVersion } from '@xmcl/modrinth' 38 | 39 | const client = new ModrinthV2Client() 40 | const projectId: string; // you can get this id from searchProjects 41 | const project: project = await client.getProject(projectId) // project details 42 | const versions: string[] = project.versions; 43 | const oneVersion: string = versions[0]; 44 | 45 | const modVersion: ModVersion = await getProjectVersion(oneVersion); 46 | 47 | const files: ProjectVersionFile[] = modVersion.files; 48 | 49 | const { url, name, hashes } = files[0]; // now you can get file name, file hashes and download url of the file 50 | ``` 51 | -------------------------------------------------------------------------------- /packages/modrinth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/modrinth", 3 | "version": "2.4.0", 4 | "main": "./index.ts", 5 | "module": "./index.ts", 6 | "sideEffects": false, 7 | "description": "An implementation of modrinth API (https://docs.modrinth.com/api-spec)", 8 | "scripts": { 9 | "build:type": "tsc", 10 | "build:cjs": "esbuild --target=node16 --external:undici --platform=node --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 11 | "build:esm": "esbuild --target=node16 --external:undici --platform=node --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts", 12 | "build:browser": "esbuild --target=es2020 --sourcemap --format=esm --alias:undici=undici-shim --bundle --outfile=dist/index.browser.mjs index.ts" 13 | }, 14 | "engines": { 15 | "node": ">=16.4" 16 | }, 17 | "publishConfig": { 18 | "access": "public", 19 | "main": "./dist/index.js", 20 | "module": "./dist/index.mjs", 21 | "browser": "./dist/index.browser.mjs" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 26 | }, 27 | "author": "cijhn@hotmail.com", 28 | "keywords": [ 29 | "minecraft", 30 | "typescript", 31 | "minecraft-launcher", 32 | "nodejs", 33 | "electron", 34 | "modrinth" 35 | ], 36 | "license": "MIT", 37 | "dependencies": { 38 | "undici": "7.2.3" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 42 | }, 43 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 44 | "devDependencies": { 45 | "esbuild": "^0.17.16", 46 | "undici-shim": "workspace:^*" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/modrinth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts", 4 | "types.ts" 5 | ], 6 | "extends": "../../tsconfig.json", 7 | "compilerOptions": { 8 | "outDir": "./dist" 9 | }, 10 | } -------------------------------------------------------------------------------- /packages/nat-api/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/nat-api/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/nat-api/README.md: -------------------------------------------------------------------------------- 1 | # nat-api 2 | 3 | Fast port mapping with **UPnP** and **NAT-PMP** in NodeJS. 4 | 5 | This is a fork version of [nat-api](https://npmjs.org/nat-api). 6 | 7 | The major differences are: 8 | 9 | - Use typescript, so has type definition. 10 | - Split PMP and upnp API, so you can use them individually. 11 | - Support ESM. 12 | - Optimize the dependencies. (only 2 up-to-dated deps) 13 | 14 | ## Install 15 | 16 | **Required: NodeJS >= 16** 17 | 18 | ```sh 19 | npm install @xmcl/nat-api 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```js 25 | const { createUpnpClient } = require('nat-api') 26 | 27 | const client = await createUpnpClient() 28 | 29 | // map 25565 to 25565 for 1 min: 30 | await client.map({ 31 | description: "Mapped by @xmcl/nat-api", 32 | protocol: 'tcp', 33 | public: 25565, 34 | private: 25565, 35 | ttl: 60 * 1000, 36 | }) 37 | 38 | // Unmap port public and private port 25565 with TCP by default 39 | await client.unmap({ port: 25565 }) 40 | 41 | // Get external IP 42 | const mappings = await client.getMappings() 43 | 44 | // see existed mappings 45 | console.log(mappings) 46 | 47 | // Destroy client 48 | client.destroy() 49 | ``` 50 | 51 | ## API 52 | 53 | See type definition in typescript. 54 | 55 | ## Additional Information 56 | 57 | - http://miniupnp.free.fr/nat-pmp.html 58 | - http://wikipedia.org/wiki/NAT_Port_Mapping_Protocol 59 | - http://tools.ietf.org/html/draft-cheshire-nat-pmp-03 60 | 61 | 62 | ## License 63 | 64 | MIT. Copyright (c) [Alex](https://github.com/alxhotel) 65 | 66 | [nat-api-ti]: https://img.shields.io/travis/com/alxhotel/nat-api/master.svg 67 | [nat-api-tu]: https://travis-ci.com/alxhotel/nat-api 68 | [nat-api-ni]: https://img.shields.io/npm/v/nat-api.svg 69 | [nat-api-nu]: https://npmjs.org/package/nat-api 70 | [nat-api-di]: https://david-dm.org/alxhotel/nat-api/status.svg 71 | [nat-api-du]: https://david-dm.org/alxhotel/nat-api 72 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 73 | [standard-url]: https://standardjs.com 74 | -------------------------------------------------------------------------------- /packages/nat-api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @xmcl/nat-api 3 | */ 4 | 5 | export * from './lib/upnp' 6 | export * from './lib/pmp' 7 | -------------------------------------------------------------------------------- /packages/nat-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.4.3", 3 | "name": "@xmcl/nat-api", 4 | "description": "Port mapping with UPnP and NAT-PMP", 5 | "main": "./index.ts", 6 | "publishConfig": { 7 | "access": "public", 8 | "main": "./dist/index.js", 9 | "module": "./dist/index.mjs" 10 | }, 11 | "scripts": { 12 | "build:cjs": "esbuild --bundle --outfile=dist/index.js --format=cjs --platform=node --external:default-gateway --external:fast-xml-parser --target=es2020 index.ts", 13 | "build:esm": "esbuild --bundle --outfile=dist/index.mjs --external:default-gateway --external:fast-xml-parser --format=esm --platform=node --target=es2020 index.ts", 14 | "build:type": "tsc" 15 | }, 16 | "engines": { 17 | "node": ">=16.0.0" 18 | }, 19 | "dependencies": { 20 | "default-gateway": "^6.0.3", 21 | "fast-xml-parser": "^4.3.2", 22 | "undici": "7.2.3" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^18.15.11", 26 | "esbuild": "^0.17.16", 27 | "typescript": "^5.3.3" 28 | }, 29 | "author": { 30 | "name": "Alex", 31 | "email": "alxmorais8@msn.com" 32 | }, 33 | "contributors": [ 34 | { 35 | "name": "CI010", 36 | "email": "cijhn@hotmail.com" 37 | } 38 | ], 39 | "license": "MIT", 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/voxelum/nat-api.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/voxelum/nat-api/issues" 46 | }, 47 | "homepage": "https://github.com/voxelum/nat-api", 48 | "keywords": [ 49 | "nat", 50 | "api", 51 | "upnp", 52 | "pmp", 53 | "nat-pmp", 54 | "holepunch", 55 | "port", 56 | "forwarding", 57 | "map", 58 | "mapping" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /packages/nat-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["lib", "index.ts"] 7 | } -------------------------------------------------------------------------------- /packages/nbt/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/nbt/README.md: -------------------------------------------------------------------------------- 1 | # Nbt Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/nbt.svg)](https://www.npmjs.com/package/@xmcl/nbt) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/nbt.svg)](https://npmjs.com/@xmcl/nbt) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/nbt)](https://packagephobia.now.sh/result?p=@xmcl/nbt) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Provide function to read NBT binary format to json. 10 | 11 | ## Usage 12 | 13 | ### Read and Write NBT 14 | 15 | You can simply deserialize/serialize nbt. 16 | 17 | ```ts 18 | import { serialize, deserialize } from "@xmcl/nbt"; 19 | const fileData: Buffer; 20 | // compressed = undefined will not perform compress algorithm 21 | // compressed = true will use gzip algorithm 22 | const compressed: true | "gzip" | "deflate" | undefined; 23 | const readed: any = await deserialize(fileData, { compressed }); 24 | // The deserialize return object contain NBTPrototype property which define its nbt type 25 | // After you do the modification on it, you can serialize it back to NBT 26 | const buf: Buffer = await serialize(readed, { compressed }); 27 | ``` 28 | 29 | You can use class with annotation (decorator) to serialize/deserialize the type consistently. 30 | 31 | Suppose you are reading the [servers.dat](https://minecraft.gamepedia.com/Servers.dat_format). You can have: 32 | 33 | ```ts 34 | import { serialize, deserialize, TagType } from "@xmcl/nbt"; 35 | 36 | class ServerInfo { 37 | @TagType(TagType.String) 38 | icon: string = ""; 39 | @TagType(TagType.String) 40 | ip: string = ""; 41 | @TagType(TagType.String) 42 | name: string = ""; 43 | @TagType(TagType.Byte) 44 | acceptTextures: number = 0; 45 | } 46 | 47 | class Servers { 48 | @TagType([ServerInfo]) 49 | servers: ServerInfo[] = [] 50 | } 51 | 52 | // read 53 | // explict tell the function to deserialize into the type Servers 54 | const servers = await deserialize(data, { type: Servers }); 55 | const infos: ServerInfo[] = servers.servers; 56 | 57 | // write 58 | const servers: Servers; 59 | const binary = await serialize(servers); 60 | ``` 61 | -------------------------------------------------------------------------------- /packages/nbt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/nbt", 3 | "version": "3.0.2", 4 | "main": "./index.ts", 5 | "browser": "./dist/index.browser.mjs", 6 | "description": "NBT serialization and deserialization", 7 | "engines": { 8 | "node": ">=16.4" 9 | }, 10 | "scripts": { 11 | "build:type": "tsc", 12 | "build:cjs": "esbuild --target=node16 --platform=node --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 13 | "build:esm": "esbuild --target=node16 --platform=node --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts", 14 | "build:browser": "esbuild --target=es2020 --external:@xmcl/* --external:pako --sourcemap --format=esm --bundle --outfile=dist/index.browser.mjs index.ts" 15 | }, 16 | "publishConfig": { 17 | "access": "public", 18 | "main": "./dist/index.js", 19 | "module": "./dist/index.mjs" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 24 | }, 25 | "author": "cijhn@hotmail.com", 26 | "keywords": [ 27 | "minecraft", 28 | "typescript", 29 | "minecraft-launcher", 30 | "nodejs", 31 | "electron", 32 | "nbt" 33 | ], 34 | "license": "MIT", 35 | "dependencies": { 36 | "@xmcl/bytebuffer": "workspace:^*", 37 | "pako": "^2.1.0" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 41 | }, 42 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 43 | "devDependencies": { 44 | "@types/node": "~18.15.11", 45 | "@types/pako": "^2.0.0", 46 | "@xmcl/oxlint-config": "workspace:^*", 47 | "esbuild": "^0.17.16", 48 | "oxlint": "^0.15.5", 49 | "typescript": "^5.3.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/nbt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "files": [ 7 | "index.ts", 8 | "utils.ts", 9 | "index.test.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/nbt/utils.ts: -------------------------------------------------------------------------------- 1 | import { ByteBuffer } from '@xmcl/bytebuffer' 2 | import type { ReadContext, WriteContext } from './index' 3 | 4 | export function writeUTF8(out: ByteBuffer, str = '', context: WriteContext) { 5 | const strlen = str.length 6 | let utflen = 0 7 | let c: number 8 | 9 | /* use charAt instead of copying String to char array */ 10 | for (let idx = 0; idx < strlen; idx++) { 11 | c = str.charCodeAt(idx) 12 | if ((c >= 0x0001) && (c <= 0x007F)) { 13 | utflen++ 14 | } else if (c > 0x07FF) { 15 | utflen += 3 16 | } else { 17 | utflen += 2 18 | } 19 | } 20 | 21 | if (utflen > 65535) { 22 | throw new RangeError( 23 | 'encoded string too long: ' + utflen + ' bytes') 24 | } 25 | 26 | out.writeInt16(utflen) 27 | if (utflen > 0) { 28 | out.writeBytes(context.encoder.encode(str)) 29 | } 30 | } 31 | 32 | export function readUTF8(buff: ByteBuffer, context: ReadContext) { 33 | const utflen = buff.readInt16() 34 | const result = context.decoder.decode(buff.buffer.slice(buff.offset, buff.offset + utflen)) 35 | buff.offset += utflen 36 | return result 37 | } 38 | -------------------------------------------------------------------------------- /packages/nbt/zlib/index.browser.ts: -------------------------------------------------------------------------------- 1 | import pako from 'pako' 2 | 3 | export function gzip(buffer: Uint8Array) { return Promise.resolve(pako.gzip(buffer)) } 4 | export function gzipSync(buffer: Uint8Array) { return pako.gzip(buffer) } 5 | export function ungzip(buffer: Uint8Array) { return Promise.resolve(pako.ungzip(buffer)) } 6 | export function gunzipSync(buffer: Uint8Array) { return pako.ungzip(buffer) } 7 | export function inflate(buffer: Uint8Array) { return Promise.resolve(pako.inflate(buffer)) } 8 | export function deflate(buffer: Uint8Array) { return Promise.resolve(pako.deflate(buffer)) } 9 | export function inflateSync(buffer: Uint8Array) { return pako.inflate(buffer) } 10 | export function deflateSync(buffer: Uint8Array) { return pako.deflate(buffer) } 11 | -------------------------------------------------------------------------------- /packages/nbt/zlib/index.ts: -------------------------------------------------------------------------------- 1 | import { deflate as ideflate, deflateSync, gunzip as igunzip, gunzipSync, gzip as igzip, gzipSync, inflate as iinflate, inflateSync } from 'zlib' 2 | import { promisify } from 'util' 3 | 4 | export const gzip: (buf: Uint8Array) => Promise = promisify(igzip) 5 | export const ungzip: (buf: Uint8Array) => Promise = promisify(igunzip) 6 | export const inflate: (buf: Uint8Array) => Promise = promisify(iinflate) 7 | export const deflate: (buf: Uint8Array) => Promise = promisify(ideflate) 8 | 9 | export { gzipSync, gunzipSync, inflateSync, deflateSync } 10 | -------------------------------------------------------------------------------- /packages/nbt/zlib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zlib", 3 | "private": true, 4 | "version": "0.0.0", 5 | "module": "./index.ts", 6 | "browser": "./index.browser.ts", 7 | "sideEffects": false, 8 | "dependencies": { 9 | "pako": "^2.1.0" 10 | }, 11 | "devDependencies": { 12 | "@types/pako": "^2.0.0" 13 | } 14 | } -------------------------------------------------------------------------------- /packages/nbt/zlib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts", 4 | "index.browser.ts" 5 | ], 6 | "compilerOptions": { 7 | "target": "es2018", 8 | "module": "esnext", 9 | "outDir": "cjs", 10 | "lib": [ 11 | "dom" 12 | ], 13 | "experimentalDecorators": true, 14 | "declaration": true, 15 | "downlevelIteration": true, 16 | "strict": true, 17 | "moduleResolution": "node", 18 | "esModuleInterop": true, 19 | "allowSyntheticDefaultImports": true 20 | } 21 | } -------------------------------------------------------------------------------- /packages/oxlint-config/.gitignore: -------------------------------------------------------------------------------- 1 | !index.js 2 | -------------------------------------------------------------------------------- /packages/oxlint-config/oxlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/oxlint/configuration_schema.json", 3 | "plugins": [ 4 | "import", 5 | "typescript", 6 | "unicorn" 7 | ], 8 | "env": { 9 | "browser": true, 10 | "node": true 11 | }, 12 | "ignorePatterns": [ 13 | "**/node_modules/**", 14 | "**/dist/**" 15 | ], 16 | "globals": {}, 17 | "settings": {}, 18 | "rules": { 19 | "eqeqeq": "warn", 20 | "import/no-cycle": "error", 21 | "@typescript-eslint/no-unsafe-declaration-merging": "off", 22 | "@typescript-eslint/member-delimiter-style": [ 23 | "error", 24 | { 25 | "multiline": { 26 | "delimiter": "none" 27 | } 28 | } 29 | ], 30 | "@typescript-eslint/ban-types": 0, 31 | "@typescript-eslint/ban-ts-comment": 0, 32 | "@typescript-eslint/explicit-function-return-type": 0, 33 | "@typescript-eslint/explicit-module-boundary-types": 0, 34 | "@typescript-eslint/no-empty-function": 0, 35 | "@typescript-eslint/no-empty-interface": 0, 36 | "@typescript-eslint/no-explicit-any": 0, 37 | "@typescript-eslint/no-non-null-assertion": 0, 38 | "@typescript-eslint/no-unused-expressions": 2, 39 | "@typescript-eslint/no-unused-vars": 0, 40 | "unicorn/no-new-array": 0, 41 | "import/no-absolute-path": 0, 42 | "space-before-function-paren": 0, 43 | "no-useless-constructor": 0, 44 | "no-unused-vars": 0, 45 | "no-use-before-define": 0, 46 | "node/no-callback-literal": 0, 47 | "no-wrapper-object-types": "off", 48 | "no-thenable": "off" 49 | }, 50 | "overrides": [ 51 | { 52 | "files": [ 53 | "*.test.ts", 54 | "*.spec.ts" 55 | ], 56 | "rules": { 57 | "@typescript-eslint/no-explicit-any": "off" 58 | } 59 | }, 60 | { 61 | "files": [ 62 | "**/xmcl-runtime/**" 63 | ], 64 | "rules": { 65 | "import/no-cycle": "off" 66 | } 67 | }, 68 | { 69 | "files": [ 70 | "**/xmcl-runtime-api/**" 71 | ], 72 | "rules": { 73 | "import/no-cycle": "off" 74 | } 75 | }, 76 | { 77 | "files": [ 78 | "**/xmcl-electron-app/**" 79 | ], 80 | "rules": { 81 | "import/no-cycle": "off" 82 | } 83 | }, 84 | { 85 | "files": [ 86 | "**/xmcl-keystone-ui/**" 87 | ], 88 | "rules": { 89 | "import/no-cycle": "off" 90 | } 91 | } 92 | ] 93 | } -------------------------------------------------------------------------------- /packages/oxlint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/oxlint-config", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "description": "The xmcl commonly used oxlint config for typescript", 7 | "dependencies": { 8 | "oxlint": "^0.15.5" 9 | } 10 | } -------------------------------------------------------------------------------- /packages/resourcepack/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/resourcepack/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The resource pack module to read Minecraft resource pack just like Minecraft in-game. 3 | * 4 | * You can open the ResourcePack by {@link ResourcePack.open} and get resource by {@link ResourcePack.get}. 5 | * 6 | * Or you can just load resource pack metadata by {@link readPackMetaAndIcon}. 7 | * 8 | * @packageDocumentation 9 | * @module @xmcl/resourcepack 10 | */ 11 | 12 | import { FileSystem, resolveFileSystem } from '@xmcl/system' 13 | import { PackMeta } from './format' 14 | 15 | export * from './resourceManager' 16 | export * from './resourcePack' 17 | export * from './modelLoader' 18 | export * from './format' 19 | 20 | /** 21 | * Read the resource pack metadata from zip file or directory. 22 | * 23 | * If you have already read the data of the zip file, you can pass it as the second parameter. The second parameter will be ignored on reading directory. 24 | * 25 | * @param resourcePack The absolute path of the resource pack file, or a buffer, or a opened resource pack. 26 | */ 27 | export async function readPackMeta(resourcePack: string | Uint8Array | FileSystem): Promise { 28 | const system = await resolveFileSystem(resourcePack) 29 | try { 30 | if (!await system.existsFile('pack.mcmeta')) { 31 | throw new Error('Illegal Resourcepack: Cannot find pack.mcmeta!') 32 | } 33 | const metadata = JSON.parse((await system.readFile('pack.mcmeta', 'utf-8')).replace(/^\uFEFF/, '')) 34 | if (!metadata.pack) { 35 | throw new Error("Illegal Resourcepack: pack.mcmeta doesn't contain the pack metadata!") 36 | } 37 | return metadata.pack 38 | } finally { 39 | if (system !== resourcePack) system.close() 40 | } 41 | } 42 | 43 | /** 44 | * Read the resource pack icon png binary. 45 | * @param resourcePack The absolute path of the resource pack file, or a buffer, or a opened resource pack. 46 | */ 47 | export async function readIcon(resourcePack: string | Uint8Array | FileSystem): Promise { 48 | const system = await resolveFileSystem(resourcePack) 49 | try { 50 | return system.readFile('pack.png') 51 | } finally { 52 | if (system !== resourcePack) system.close() 53 | } 54 | } 55 | 56 | /** 57 | * Read both metadata and icon 58 | * 59 | * @see {@link readIcon} 60 | * @see {@link readPackMeta} 61 | */ 62 | export async function readPackMetaAndIcon(resourcePack: string | Uint8Array | FileSystem) { 63 | const system = await resolveFileSystem(resourcePack) 64 | 65 | try { 66 | return { 67 | metadata: await readPackMeta(system), 68 | icon: await readIcon(system).catch(() => undefined), 69 | } 70 | } finally { 71 | if (system !== resourcePack) system.close() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/resourcepack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/resourcepack", 3 | "version": "1.2.4", 4 | "main": "./index.ts", 5 | "description": "A Minecraft resource pack parser", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=node16 --platform=node --external:@xmcl/* --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=node16 --platform=node --external:@xmcl/* --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "access": "public", 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs" 18 | }, 19 | "dependencies": { 20 | "@xmcl/system": "workspace:^*" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 25 | }, 26 | "author": "cijhn@hotmail.com", 27 | "keywords": [ 28 | "minecraft", 29 | "typescript", 30 | "minecraft-launcher", 31 | "nodejs", 32 | "electron", 33 | "resourcepack" 34 | ], 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 38 | }, 39 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 40 | "devDependencies": { 41 | "esbuild": "^0.17.16" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/resourcepack/resourceManager.ts: -------------------------------------------------------------------------------- 1 | import { PackMeta } from './format' 2 | import { ResourcePack, Resource, ResourceLocation } from './resourcePack' 3 | 4 | export interface ResourcePackWrapper { 5 | source: ResourcePack 6 | info: PackMeta.Pack 7 | domains: string[] 8 | } 9 | 10 | export interface ResourceLoader { 11 | /** 12 | * Get the resource in that location. This will walk through current resource source list to load the resource. 13 | * @param location The resource location 14 | */ 15 | get(location: ResourceLocation): Promise 16 | } 17 | 18 | /** 19 | * The resource manager just like Minecraft. Design to be able to use in both nodejs and browser environment. 20 | */ 21 | export class ResourceManager implements ResourceLoader { 22 | constructor( 23 | /** 24 | * The list order is just like the order in options.txt. The last element is the highest priority one. 25 | * The resource will load from the last one to the first one. 26 | */ 27 | public list: Array = []) { } 28 | 29 | get allResourcePacks() { return this.list.map((l) => l.info) } 30 | 31 | /** 32 | * Add a new resource source as the first priority of the resource list. 33 | */ 34 | async addResourcePack(resourcePack: ResourcePack) { 35 | let info 36 | try { 37 | info = await resourcePack.info() 38 | } catch { 39 | info = { pack_format: -1, description: '' } 40 | } 41 | const domains = await resourcePack.domains() 42 | const wrapper = { info, source: resourcePack, domains } 43 | 44 | this.list.push(wrapper) 45 | 46 | return wrapper 47 | } 48 | 49 | remove(index: number) { 50 | return this.list.splice(index, 1)[0] 51 | } 52 | 53 | /** 54 | * Clear all resource packs in this manager 55 | */ 56 | clear() { 57 | return this.list.splice(0, this.list.length) 58 | } 59 | 60 | /** 61 | * Swap the resource source priority. 62 | */ 63 | swap(first: number, second: number) { 64 | if (first >= this.list.length || first < 0 || second >= this.list.length || second < 0) { 65 | throw new Error('Illegal index') 66 | } 67 | 68 | const fir = this.list[first] 69 | this.list[first] = this.list[second] 70 | this.list[second] = fir 71 | } 72 | 73 | /** 74 | * Get the resource in that location. This will walk through current resource source list to load the resource. 75 | * @param location The resource location 76 | */ 77 | async get(location: ResourceLocation): Promise { 78 | for (let i = this.list.length - 1; i >= 0; i--) { 79 | const src = this.list[i] 80 | const resource = await src.source.get(location) 81 | if (resource) { return resource } 82 | } 83 | return undefined 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/resourcepack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "files": [ 7 | "format.ts", 8 | "index.ts", 9 | "modelLoader.ts", 10 | "resourceManager.ts", 11 | "resourcePack.ts" 12 | ], 13 | } -------------------------------------------------------------------------------- /packages/semver/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/semver/README.md: -------------------------------------------------------------------------------- 1 | # Fabric Semetic Version Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/semver.svg)](https://www.npmjs.com/package/@xmcl/semver) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/semver.svg)](https://npmjs.com/@xmcl/semver) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/semver)](https://packagephobia.now.sh/result?p=@xmcl/semver) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Port the fabric special [sementic version algorithm](https://github.com/FabricMC/fabric-loader/tree/master/src/main/java/net/fabricmc/loader/impl/util/version) to typescript. 10 | 11 | 12 | ```ts 13 | import { parseVersionRange, FabricSemanticVersion } from "@xmcl/semver"; 14 | 15 | const versionRangeString = ">=1.0+fabric+minecraft"; // this is invalid as a normal semver but valid here 16 | const versionRange = parseVersionRange(versionRangeString); 17 | 18 | const versionString = "1.21"; // a Minecraft version 19 | const semver = parseSemanticVersion(versionString); 20 | 21 | const isVersionInRange = versionRange.test(semver); // is version in this version range 22 | ``` 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/semver/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @xmcl/semver 3 | */ 4 | export * from './semver' 5 | export * from './range' -------------------------------------------------------------------------------- /packages/semver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/semver", 3 | "version": "0.1.1", 4 | "main": "./index.ts", 5 | "author": { 6 | "name": "cijhn@hotmail.com" 7 | }, 8 | "description": "The special semver format using by fabricmc.", 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=es2020 --platform=neutral --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=es2020 --platform=neutral --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "main": "./dist/index.js", 16 | "module": "./dist/index.mjs", 17 | "access": "public" 18 | }, 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "~18.15.11", 26 | "@xmcl/oxlint-config": "workspace:^*", 27 | "esbuild": "^0.17.16", 28 | "oxlint": "^0.15.5", 29 | "typescript": "^5.3.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/semver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts", 4 | "range.ts", 5 | "operators.ts", 6 | "semver.ts" 7 | ], 8 | "extends": "../../tsconfig.json", 9 | "compilerOptions": { 10 | "outDir": "./dist" 11 | }, 12 | } -------------------------------------------------------------------------------- /packages/system/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/system/README.md: -------------------------------------------------------------------------------- 1 | # Common Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/system.svg)](https://www.npmjs.com/package/@xmcl/system) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/system.svg)](https://npmjs.com/@xmcl/system) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/system)](https://packagephobia.now.sh/result?p=@xmcl/system) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | A unified API to read directory or zip. 10 | 11 | Support both nodejs and browser. 12 | 13 | You can do read operations for zip or directory in same API: 14 | 15 | ```ts 16 | import { openFileSystem } from "@xmcl/system"; 17 | 18 | let filePath = "/path/to/dir/" 19 | const fs = await openFileSystem(filePath); 20 | fs.readFile("a.txt"); // read /path/to/dir/a.txt 21 | 22 | let zipPath = "/path/to/file.zip" 23 | const fs = await openFileSystem(zipPath); 24 | fs.readFile("a.txt"); // read a.txt in the file.zip! 25 | ``` 26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/system/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/system", 3 | "version": "2.2.8", 4 | "main": "./index.ts", 5 | "browser": "./index.browser.ts", 6 | "description": "An abstract layer for file system on reading zip or plain dictionary/file", 7 | "engines": { 8 | "node": ">=20" 9 | }, 10 | "scripts": { 11 | "build:type": "tsc", 12 | "build:cjs": "esbuild --target=node16 --external:yauzl --external:@xmcl/* --platform=node --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 13 | "build:esm": "esbuild --target=node16 --external:yauzl --external:@xmcl/* --platform=node --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts", 14 | "build:browser": "esbuild --target=es2020 --sourcemap --external:jszip --external:@xmcl/* --format=esm --bundle --outfile=dist/index.browser.mjs index.browser.ts" 15 | }, 16 | "publishConfig": { 17 | "main": "./dist/index.js", 18 | "browser": "./dist/index.browser.mjs", 19 | "module": "./dist/index.mjs", 20 | "access": "public" 21 | }, 22 | "dependencies": { 23 | "@xmcl/unzip": "workspace:^*", 24 | "yauzl": "^2.10.0", 25 | "jszip": "^3.10.1" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 30 | }, 31 | "author": "cijhn@hotmail.com", 32 | "keywords": [ 33 | "minecraft", 34 | "typescript", 35 | "minecraft-launcher", 36 | "nodejs", 37 | "file-system", 38 | "electron" 39 | ], 40 | "devDependencies": { 41 | "@types/node": "~18.15.11", 42 | "@types/yauzl": "^2.10.0", 43 | "@xmcl/oxlint-config": "workspace:^*", 44 | "esbuild": "^0.17.16", 45 | "oxlint": "^0.15.5", 46 | "typescript": "^5.3.3" 47 | }, 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 51 | }, 52 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme" 53 | } 54 | -------------------------------------------------------------------------------- /packages/system/system.ts: -------------------------------------------------------------------------------- 1 | export abstract class FileSystem { 2 | abstract readonly root: string 3 | abstract readonly sep: string 4 | abstract readonly type: 'zip' | 'path' 5 | abstract readonly writeable: boolean 6 | 7 | // base methods 8 | 9 | abstract join(...paths: string[]): string 10 | 11 | abstract isDirectory(name: string): Promise 12 | abstract existsFile(name: string): Promise 13 | abstract readFile(name: string, encoding: 'utf-8' | 'base64'): Promise 14 | abstract readFile(name: string, encoding: undefined): Promise 15 | abstract readFile(name: string): Promise 16 | abstract readFile(name: string, encoding?: 'utf-8' | 'base64'): Promise 17 | 18 | /** 19 | * Get the url for a file entry. If the system does not support get url. This should return an empty string. 20 | */ 21 | getUrl(name: string): string { return '' } 22 | 23 | abstract listFiles(name: string): Promise 24 | 25 | abstract cd(name: string): void 26 | 27 | isClosed(): boolean { return false } 28 | close(): void { } 29 | 30 | // extension methods 31 | 32 | async missingFile(name: string) { 33 | return this.existsFile(name).then((v) => !v) 34 | } 35 | 36 | async walkFiles(target: string, walker: (path: string) => void | Promise) { 37 | if (await this.isDirectory(target)) { 38 | const childs = await this.listFiles(target) 39 | for (const child of childs) { 40 | await this.walkFiles(this.join(target, child), walker) 41 | } 42 | } else { 43 | const result = walker(this.join(target)) 44 | if (result instanceof Promise) { 45 | await result 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/system/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts", 4 | "system.ts", 5 | "index.browser.ts" 6 | ], 7 | "extends": "../../tsconfig.json", 8 | "compilerOptions": { 9 | "outDir": "./dist" 10 | }, 11 | } -------------------------------------------------------------------------------- /packages/task/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/task/README.md: -------------------------------------------------------------------------------- 1 | # Task Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/task.svg)](https://www.npmjs.com/package/@xmcl/task) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/task.svg)](https://npmjs.com/@xmcl/task) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/task)](https://packagephobia.now.sh/result?p=@xmcl/task) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | This is a helper module for Minecraft Launcher. See the github home page for more information. 10 | 11 | ## Usage 12 | 13 | ### Progress Moniting 14 | 15 | You can use `@xmcl/task` model to track the progress of a task. *In the launcher, they are majorly download task.* 16 | 17 | This module implements a basic object model for task with progress. The task can be paused or cancelled. 18 | 19 | ```ts 20 | import { Task, TaskBase, task } from "@xmcl/task"; 21 | 22 | class ATask extends TaskBase { 23 | // implement a task 24 | } 25 | 26 | class BTask extends TaskBase { 27 | // implement a task 28 | } 29 | 30 | // suppose you have such task 31 | const myTask = task("hello", function() { 32 | await this.yield(new ATask().setName("world")); 33 | await this.yield(new BTask().setName("xmcl")); 34 | }); 35 | 36 | // start a task 37 | const result = await task.startAndWait({ 38 | onStart(task: Task) { 39 | // the task path is the task name joined by dot (.) 40 | const path = task.path; 41 | console.log(`${path} started!`); 42 | }, 43 | onUpdate(task: Task, chunkSize: number) { 44 | // a task update 45 | }, 46 | onFailed(task: Task, error: any) { 47 | // on a task fail 48 | }, 49 | onSucceed(task: Task, result: any) { 50 | // on task success 51 | const path = task.path; 52 | console.log(`${path} ended!`); 53 | }, 54 | // on task is paused/resumed/cancelled 55 | onPaused(task: Task) { }, 56 | onResumed(task: Task) { }, 57 | onCancelled(task: Task) { }, 58 | }); 59 | // the result will print like 60 | // hello started! 61 | // hello.world started! 62 | // hello.world ended! 63 | // hello.xmcl started! 64 | // hello.xmcl ended! 65 | // hello ended! 66 | ``` 67 | -------------------------------------------------------------------------------- /packages/task/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/task", 3 | "version": "4.1.1", 4 | "main": "./index.ts", 5 | "author": { 6 | "name": "cijhn@hotmail.com" 7 | }, 8 | "description": "A task interface using tree structure.", 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=es2020 --platform=neutral --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=es2020 --platform=neutral --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "main": "./dist/index.js", 16 | "module": "./dist/index.mjs", 17 | "access": "public" 18 | }, 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "~18.15.11", 26 | "@xmcl/oxlint-config": "workspace:^*", 27 | "esbuild": "^0.17.16", 28 | "oxlint": "^0.15.5", 29 | "typescript": "^5.3.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/task/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts" 4 | ], 5 | "extends": "../../tsconfig.json", 6 | "compilerOptions": { 7 | "outDir": "./dist" 8 | }, 9 | } -------------------------------------------------------------------------------- /packages/text-component/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/text-component/README.md: -------------------------------------------------------------------------------- 1 | # Text-component Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/text-component.svg)](https://www.npmjs.com/package/@xmcl/text-component) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/text-component.svg)](https://npmjs.com/@xmcl/text-component) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/text-component)](https://packagephobia.now.sh/result?p=@xmcl/text-component) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | Provide functions to parse Minecraft text component. 10 | 11 | ## Usage 12 | 13 | ### TextComponent 14 | 15 | Create TextComponent from string OR Minecraft's formatted string, like `'§cThis is red'`: 16 | 17 | ```ts 18 | import { TextComponent, fromFormattedString } from "@xmcl/text-component"; 19 | const formattedString: string; 20 | const fromFormatted: TextComponent = fromFormattedString(formattedString); 21 | ``` 22 | 23 | Render the TextComponent to css: 24 | 25 | ```ts 26 | import { TextComponent, render, RenderNode } from "@xmcl/text-component"; 27 | const yourComponent: TextComponent; 28 | const node: RenderNode = render(yourComponent); 29 | 30 | node.text; // the text of the node 31 | node.style; // style of the node 32 | node.children; // children 33 | 34 | // you can render in dom like this: 35 | 36 | function renderToDom(node: RenderNode) { 37 | const span = document.createElement('span'); 38 | span.style = node.style; 39 | span.textContent = node.text; 40 | for (const child of node.children) { 41 | span.appendChild(renderToDom(child)); 42 | } 43 | } 44 | ``` 45 | 46 | Iterate the TextComponent and its children: 47 | 48 | ```ts 49 | import { TextComponent, flat } from "@xmcl/text-component"; 50 | const yourComponent: TextComponent; 51 | const selfAndAllChildren: Array = flat(yourComponent); 52 | ``` 53 | -------------------------------------------------------------------------------- /packages/text-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/text-component", 3 | "version": "2.1.3", 4 | "main": "./index.ts", 5 | "description": "Parse Minecraft text component and render it to css.", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=es2020 --platform=neutral --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=es2020 --platform=neutral --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "access": "public", 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 22 | }, 23 | "author": "cijhn@hotmail.com", 24 | "sideEffects": false, 25 | "keywords": [ 26 | "minecraft", 27 | "typescript", 28 | "minecraft-launcher", 29 | "nodejs", 30 | "text-component", 31 | "electron" 32 | ], 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 36 | }, 37 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 38 | "devDependencies": { 39 | "@types/node": "~18.15.11", 40 | "@xmcl/oxlint-config": "workspace:^*", 41 | "esbuild": "^0.17.16", 42 | "oxlint": "^0.15.5", 43 | "typescript": "^5.3.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/text-component/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts" 4 | ], 5 | "extends": "../../tsconfig.json", 6 | "compilerOptions": { 7 | "outDir": "./dist" 8 | }, 9 | } -------------------------------------------------------------------------------- /packages/undici-shim/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-redeclare, n/no-unsupported-features/node-builtins 2 | export const fetch = globalThis.fetch 3 | // eslint-disable-next-line n/no-unsupported-features/node-builtins, @typescript-eslint/no-redeclare 4 | export const File = globalThis.File 5 | // eslint-disable-next-line n/no-unsupported-features/node-builtins, @typescript-eslint/no-redeclare 6 | export const FormData = globalThis.FormData 7 | -------------------------------------------------------------------------------- /packages/undici-shim/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "undici-shim", 3 | "version": "0.0.2", 4 | "private": true, 5 | "module": "index.ts" 6 | } 7 | -------------------------------------------------------------------------------- /packages/undici-shim/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts" 4 | ], 5 | "engines": { 6 | "node": ">=20" 7 | }, 8 | "compilerOptions": { 9 | "target": "es2018", 10 | "module": "esnext", 11 | "lib": [ 12 | "DOM" 13 | ], 14 | "experimentalDecorators": true, 15 | "sourceMap": true, 16 | "declaration": true, 17 | "downlevelIteration": true, 18 | "strict": true, 19 | "moduleResolution": "node", 20 | "esModuleInterop": true, 21 | "allowSyntheticDefaultImports": true 22 | } 23 | } -------------------------------------------------------------------------------- /packages/unzip/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/unzip/README.md: -------------------------------------------------------------------------------- 1 | # Unzip Module 2 | 3 | [![npm version](https://img.shields.io/npm/v/@xmcl/unzip.svg)](https://www.npmjs.com/package/@xmcl/unzip) 4 | [![Downloads](https://img.shields.io/npm/dm/@xmcl/unzip.svg)](https://npmjs.com/@xmcl/unzip) 5 | [![Install size](https://packagephobia.now.sh/badge?p=@xmcl/unzip)](https://packagephobia.now.sh/result?p=@xmcl/unzip) 6 | [![npm](https://img.shields.io/npm/l/@xmcl/minecraft-launcher-core.svg)](https://github.com/voxelum/minecraft-launcher-core-node/blob/master/LICENSE) 7 | [![Build Status](https://github.com/voxelum/minecraft-launcher-core-node/workflows/Build/badge.svg)](https://github.com/Voxelum/minecraft-launcher-core-node/actions?query=workflow%3ABuild) 8 | 9 | A simple Unzipper wrapper for yauzl in nodejs. 10 | 11 | Support `Promise` and nodejs `Stream`. 12 | 13 | ## Who might care this package 14 | 15 | The people 16 | 17 | 1. who use nodejs 18 | 2. who want a unzip only interface 19 | 3. who think yauzl is good but its API is hard to use 20 | 21 | might want to look at this. 22 | 23 | ## Where is the document 24 | 25 | Since this is majorly used in its [parent project](https://github.com/voxelum/minecraft-launcher-core-node). You can 26 | -------------------------------------------------------------------------------- /packages/unzip/index.test.ts: -------------------------------------------------------------------------------- 1 | import { open } from './index' 2 | import { describe, test, expect, vi } from 'vitest' 3 | import { open as yopen, fromBuffer, fromFd } from 'yauzl' 4 | 5 | vi.mock('yauzl', () => { 6 | return { 7 | open: vi.fn(), 8 | fromBuffer: vi.fn(), 9 | fromFd: vi.fn(), 10 | } 11 | }) 12 | 13 | describe('Unzip', () => { 14 | describe('#open', () => { 15 | test('should call yauzl open', async () => { 16 | const value = 'test.zip' 17 | let target: any 18 | let options: any 19 | const ret: any = {} 20 | // @ts-ignore 21 | vi.mocked(yopen).mockImplementationOnce((_target, _options, cb) => { 22 | target = _target 23 | options = _options 24 | cb(undefined, ret) 25 | }) 26 | await expect(open(value)).resolves.toEqual(ret) 27 | expect(target).toEqual('test.zip') 28 | expect(options).toEqual({ autoClose: false, lazyEntries: true }) 29 | }) 30 | test('should call yauzl fromBuffer', async () => { 31 | const buff = Buffer.from([0]) 32 | let target: any 33 | let options: any 34 | const ret: any = {} 35 | // @ts-ignore 36 | vi.mocked(fromBuffer).mockImplementationOnce((_target, _options, cb) => { 37 | target = _target 38 | options = _options 39 | cb(undefined, ret) 40 | }) 41 | await expect(open(buff)).resolves.toEqual(ret) 42 | expect(target).toEqual(buff) 43 | expect(options).toEqual({ autoClose: false, lazyEntries: true }) 44 | }) 45 | test('should call yauzl fromFd', async () => { 46 | const value = 1 47 | let target: any 48 | let options: any 49 | const ret: any = {} 50 | // @ts-ignore 51 | vi.mocked(fromFd).mockImplementationOnce((_target: number, _options: any, cb: any) => { 52 | target = _target 53 | options = _options 54 | cb(undefined, ret) 55 | }) 56 | await expect(open(value)).resolves.toEqual(ret) 57 | expect(target).toEqual(value) 58 | expect(options).toEqual({ autoClose: false, lazyEntries: true }) 59 | }) 60 | test('should catch error', async () => { 61 | // @ts-ignore 62 | vi.mocked(yopen).mockImplementationOnce((_target: any, _options: any, cb: any) => { 63 | cb(new Error('ERROR!')) 64 | }) 65 | await expect(open('test.zip')).rejects.toEqual(new Error('ERROR!')) 66 | // @ts-ignore 67 | vi.mocked(yopen).mockImplementationOnce((_target: any, _options: any, cb: any) => { 68 | cb(undefined) 69 | }) 70 | await expect(open('test.zip')).rejects.toEqual(Object.assign(new Error('Fail to open zip file'), { name: 'InvalidZipFile' })) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /packages/unzip/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/unzip", 3 | "version": "2.1.2", 4 | "main": "./index.ts", 5 | "description": "A easy unzip interface based on yauzl", 6 | "engines": { 7 | "node": ">=20" 8 | }, 9 | "scripts": { 10 | "build:type": "tsc", 11 | "build:cjs": "esbuild --target=node16 --platform=node --external:yauzl --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 12 | "build:esm": "esbuild --target=node16 --platform=node --external:yauzl --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts" 13 | }, 14 | "publishConfig": { 15 | "main": "./dist/index.js", 16 | "module": "./dist/index.mjs", 17 | "access": "public" 18 | }, 19 | "peerDependencies": { 20 | "yauzl": "^2.10.0" 21 | }, 22 | "dependencies": { 23 | "@types/yauzl": "^2.10.0", 24 | "yauzl": "^2.10.0" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 29 | }, 30 | "author": "cijhn@hotmail.com", 31 | "sideEffects": false, 32 | "keywords": [ 33 | "typescript", 34 | "nodejs", 35 | "yauzl", 36 | "unzip", 37 | "zlib", 38 | "extract" 39 | ], 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 43 | }, 44 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 45 | "devDependencies": { 46 | "@xmcl/oxlint-config": "workspace:^*", 47 | "esbuild": "^0.17.16", 48 | "typescript": "^5.3.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/unzip/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts" 4 | ], 5 | "extends": "../../tsconfig.json", 6 | "compilerOptions": { 7 | "outDir": "./dist" 8 | }, 9 | } -------------------------------------------------------------------------------- /packages/user-offline-uuid/index.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto' 2 | 3 | export function getOfflineUUID(username: string) { 4 | const md5Bytes = createHash('md5').update(`OfflinePlayer:${username}`).digest() 5 | md5Bytes[6] &= 0x0f /* clear version */ 6 | md5Bytes[6] |= 0x30 /* set to version 3 */ 7 | md5Bytes[8] &= 0x3f /* clear variant */ 8 | md5Bytes[8] |= 0x80 /* set to IETF variant */ 9 | return md5Bytes.toString('hex').replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, '$1-$2-$3-$4-$5') 10 | } 11 | -------------------------------------------------------------------------------- /packages/user-offline-uuid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-offline-uuid", 3 | "version": "0.1.0", 4 | "private": true, 5 | "module": "index.ts", 6 | "browser": "index.browser.ts", 7 | "dependencies": { 8 | "undici": "7.2.3", 9 | "@types/node": "~18.15.11" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/user-offline-uuid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "index.ts", 4 | "index.browser.ts" 5 | ], 6 | "compilerOptions": { 7 | "target": "es2018", 8 | "module": "esnext", 9 | "lib": ["DOM"], 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "declaration": true, 13 | "downlevelIteration": true, 14 | "strict": true, 15 | "moduleResolution": "node", 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true 18 | } 19 | } -------------------------------------------------------------------------------- /packages/user/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.d.ts 2 | *.test.js 3 | *.test.js.map 4 | *.test.ts 5 | test.d.ts 6 | test.js 7 | test.js.map 8 | test.ts 9 | *.ts 10 | !*.d.ts 11 | *.log 12 | tsconfig.json 13 | -------------------------------------------------------------------------------- /packages/user/gameProfile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The game profile of the user. 3 | * 4 | * In auth response, it will usually carry the `userId`, `createdAt` properties. 5 | * 6 | * In `lookup` function, it will carry the `properties` property. 7 | */ 8 | export interface GameProfile { 9 | /** 10 | * game profile unique id 11 | */ 12 | id: string 13 | /** 14 | * This is in game displayed name 15 | */ 16 | name: string 17 | properties?: { [name: string]: string } 18 | userId?: string 19 | createdAt?: number 20 | legacyProfile?: boolean 21 | suspended?: boolean 22 | paid?: boolean 23 | migrated?: boolean 24 | legacy?: boolean 25 | } 26 | 27 | export interface GameProfileWithProperties extends GameProfile { 28 | properties: { [name: string]: string } 29 | } 30 | -------------------------------------------------------------------------------- /packages/user/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @xmcl/user 3 | */ 4 | export * from './offline' 5 | export { GameProfile, GameProfileWithProperties } from './gameProfile' 6 | export * from './mojang' 7 | export * from './yggdrasil' 8 | export * from './microsoft' 9 | -------------------------------------------------------------------------------- /packages/user/microsoft.test.ts: -------------------------------------------------------------------------------- 1 | import { MockAgent, fetch as _fetch } from 'undici' 2 | import { describe, expect, it } from 'vitest' 3 | import { MicrosoftAuthenticator } from './microsoft' 4 | 5 | describe('MicrosoftAuthenticator', () => { 6 | const agent = new MockAgent() 7 | const fetch: typeof globalThis.fetch = (input, init) => { 8 | init = Object.assign(init || {}, { 9 | dispatcher: agent, 10 | }) 11 | return _fetch(input as any, init as any) as any 12 | } 13 | 14 | describe('#authenticateXboxLive', () => { 15 | it('should be able to authenticate ', async () => { 16 | const pool = agent.get('https://user.auth.xboxlive.com') 17 | pool.intercept({ 18 | method: 'POST', 19 | path: '/user/authenticate', 20 | body: JSON.stringify({ 21 | Properties: { 22 | AuthMethod: 'RPS', 23 | SiteName: 'user.auth.xboxlive.com', 24 | RpsTicket: 'd=ci010', 25 | }, 26 | RelyingParty: 'http://auth.xboxlive.com', 27 | TokenType: 'JWT', 28 | }), 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | }, 32 | }).reply(200, { hello: 'good' }) 33 | const client = new MicrosoftAuthenticator({ fetch }) 34 | await expect(client.authenticateXboxLive('ci010')) 35 | .resolves.toEqual({ hello: 'good' }) 36 | }) 37 | }) 38 | 39 | it('should return XSTS token', async () => { 40 | // Arrange 41 | const xblResponseToken = 'some-token' 42 | const relyingParty = 'rp://api.minecraftservices.com/' 43 | const expectedToken = 'some-xsts-token' 44 | 45 | agent.get('https://xsts.auth.xboxlive.com') 46 | .intercept({ 47 | method: 'POST', 48 | path: '/xsts/authorize', 49 | body: JSON.stringify({ 50 | Properties: { 51 | SandboxId: 'RETAIL', 52 | UserTokens: [xblResponseToken], 53 | }, 54 | RelyingParty: relyingParty, 55 | TokenType: 'JWT', 56 | }), 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | }, 60 | }) 61 | .reply(200, { Token: expectedToken }) 62 | 63 | // Act 64 | const client = new MicrosoftAuthenticator({ fetch }) 65 | const result = await client.authorizeXboxLive(xblResponseToken, relyingParty) 66 | 67 | // Assert 68 | expect(result.Token).to.equal(expectedToken) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /packages/user/offline.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { offline } from './offline' 3 | 4 | describe('#offline', () => { 5 | it('should be able to login ', () => { 6 | const offlineUser = offline('ci010') 7 | expect(offlineUser.selectedProfile.name).toEqual('ci010') 8 | expect(offlineUser.selectedProfile.id).toEqual('942a4c3f-6815-30e4-8e8e-f00758a9128c') 9 | expect(offlineUser.accessToken).toBeTruthy() 10 | expect(offlineUser.clientToken).toBeTruthy() 11 | expect(offlineUser.user!.id).toBeTruthy() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /packages/user/offline.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid' 2 | import { getOfflineUUID } from 'user-offline-uuid' 3 | 4 | /** 5 | * Random generate a new token by uuid v4. It can be client or auth token. 6 | * @returns a new token 7 | */ 8 | export function newToken() { 9 | return v4().replace(/-/g, '') 10 | } 11 | 12 | export { getOfflineUUID } 13 | 14 | /** 15 | * Create an offline auth. It'll ensure the user game profile's `uuid` is the same for the same `username`. 16 | * 17 | * @param username The username you want to have in-game. 18 | */ 19 | export function offline(username: string, uuid?: string) { 20 | const id = (uuid || getOfflineUUID(username)) 21 | const prof = { 22 | id, 23 | name: username, 24 | } 25 | return { 26 | accessToken: newToken(), 27 | clientToken: newToken(), 28 | selectedProfile: prof, 29 | availableProfiles: [prof], 30 | user: { 31 | id, 32 | username, 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/user/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmcl/user", 3 | "version": "4.2.0", 4 | "main": "./index.ts", 5 | "description": "Minecraft user related functions, including Yggdrasil authenticator, player skin fetcher, and Mojang security API", 6 | "scripts": { 7 | "build:type": "tsc", 8 | "build:cjs": "esbuild --target=node16 --external:undici --external:uuid --platform=node --sourcemap --format=cjs --bundle --outfile=dist/index.js index.ts", 9 | "build:esm": "esbuild --target=node16 --external:undici --external:uuid --platform=node --sourcemap --format=esm --bundle --outfile=dist/index.mjs index.ts", 10 | "build:browser": "esbuild --target=es2020 --sourcemap --external:uuid --alias:undici=undici-shim --format=esm --bundle --outfile=dist/index.browser.mjs index.ts" 11 | }, 12 | "engines": { 13 | "node": ">=20" 14 | }, 15 | "publishConfig": { 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs", 18 | "browser": "./dist/index.browser.mjs", 19 | "access": "public" 20 | }, 21 | "dependencies": { 22 | "undici": "7.2.3", 23 | "uuid": "^9.0.0" 24 | }, 25 | "sideEffects": false, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/Voxelum/minecraft-launcher-core-node.git" 29 | }, 30 | "author": "cijhn@hotmail.com", 31 | "keywords": [ 32 | "minecraft", 33 | "typescript", 34 | "minecraft-launcher", 35 | "nodejs", 36 | "electron", 37 | "yggdrasil-authentication", 38 | "minecraft-skin", 39 | "microsoft-authentication" 40 | ], 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/Voxelum/minecraft-launcher-core-node/issues" 44 | }, 45 | "homepage": "https://github.com/Voxelum/minecraft-launcher-core-node#readme", 46 | "devDependencies": { 47 | "@types/uuid": "^9.0.1", 48 | "esbuild": "^0.17.16", 49 | "typescript": "^5.3.3", 50 | "user-offline-uuid": "workspace:^*", 51 | "undici-shim": "workspace:^*" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/user/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "files": [ 4 | "index.ts", 5 | "offline.ts", 6 | "mojang.ts", 7 | "gameProfile.ts", 8 | "microsoft.ts", 9 | "yggdrasil.ts" 10 | ], 11 | "compilerOptions": { 12 | "outDir": "./dist" 13 | } 14 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**' 3 | - '.github/actions/prepare-pr' -------------------------------------------------------------------------------- /protocol.json: -------------------------------------------------------------------------------- 1 | { 2 | "338": ["1.12.1"], 3 | "335": ["1.12"], 4 | "316": ["1.11.2", "1.11.1"], 5 | "315": ["1.11"], 6 | "210": ["1.10.2", "1.10.1", "1.10"], 7 | "110": ["1.9.4", "1.9.3"], 8 | "109": ["1.9.2"], 9 | "108": ["1.9.1"], 10 | "107": ["1.9"], 11 | "47": ["1.8.9", "1.8.8", "1.8.7", "1.8.6", "1.8.5", "1.8.4", "1.8.3", "1.8.2", "1.8.1", "1.8"], 12 | "5": ["1.7.10", "1.7.9", "1.7.8", "1.7.7", "1.7.6"], 13 | "4": ["1.7.5", "1.7.4", "1.7.2"] 14 | } -------------------------------------------------------------------------------- /test-setup.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { beforeEach } from 'vitest' 3 | 4 | beforeEach((context) => { 5 | context.mock = join(__dirname, 'mock') 6 | context.temp = join(__dirname, 'temp') 7 | }) 8 | -------------------------------------------------------------------------------- /test.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vitest' { 2 | export interface TestContext { 3 | mock: string 4 | temp: string 5 | } 6 | } 7 | export {} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "forceConsistentCasingInFileNames": true, 5 | "experimentalDecorators": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true, 8 | "declaration": true, 9 | "downlevelIteration": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "inlineSourceMap": true, 13 | "inlineSources": true, 14 | "moduleResolution": "node", 15 | "esModuleInterop": true, 16 | "allowSyntheticDefaultImports": true 17 | }, 18 | "exclude": [ 19 | "docs/**/*", 20 | "packages/**/*.d.ts", 21 | "node_modules/**/*" 22 | ], 23 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import { join } from 'path' 3 | 4 | export default defineConfig({ 5 | test: { 6 | setupFiles: [join(__dirname, './test-setup.ts')], 7 | sequence: { 8 | 9 | }, 10 | globals: true, 11 | coverage: { 12 | provider: 'v8', 13 | }, 14 | }, 15 | }) 16 | --------------------------------------------------------------------------------